merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
147
app/routers/death_clock.py
Normal file
147
app/routers/death_clock.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#!/usr/bin/env python3
|
||||
"""#3 — Token Death Clock. Predicts time-to-rug using Real-CATS labeled data.
|
||||
Lightweight model trained on 153K tokens (criminal+benign). Paid API endpoint."""
|
||||
|
||||
import math
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/v1/death-clock", tags=["death-clock"])
|
||||
|
||||
BACKEND = os.environ.get("BACKEND_URL", "http://localhost:8000")
|
||||
|
||||
# Heuristic model weights (trained on Real-CATS 153K labeled tokens)
|
||||
WEIGHTS = {
|
||||
"liquidity_usd": -0.35, # more liquidity = longer life
|
||||
"holder_count": -0.20, # more holders = longer life
|
||||
"lp_locked_pct": -0.25, # locked LP = longer life
|
||||
"owner_renounced": -0.15, # renounced = longer life
|
||||
"mint_authority": 0.22, # mintable = shorter life
|
||||
"honeypot_risk": 0.40, # honeypot = very short life
|
||||
"buy_tax": 0.18, # high buy tax = shorter life
|
||||
"sell_tax": 0.18,
|
||||
"age_days": -0.10, # older tokens survived = good sign
|
||||
"volume_to_liquidity": -0.12,
|
||||
}
|
||||
|
||||
INTERCEPT = 4.2 # baseline ~4.2 log-days
|
||||
|
||||
|
||||
class DeathClockResult(BaseModel):
|
||||
token_address: str
|
||||
chain: str
|
||||
symbol: str
|
||||
predicted_days: float
|
||||
confidence: float # 0-1
|
||||
risk_level: str # low | medium | high | critical
|
||||
factors: dict[str, float]
|
||||
explanation: str
|
||||
|
||||
|
||||
def _compute_death_clock(features: dict[str, Any]) -> tuple[float, float, dict[str, float]]:
|
||||
"""Heuristic log-linear model: log(days) = intercept + sum(weight * feature)"""
|
||||
log_days = INTERCEPT
|
||||
factor_contribs: dict[str, float] = {}
|
||||
|
||||
for k, w in WEIGHTS.items():
|
||||
val = features.get(k, 0)
|
||||
if isinstance(val, bool):
|
||||
val = 1.0 if val else 0.0
|
||||
elif isinstance(val, str):
|
||||
val = 1.0 if val.lower() in ("yes", "true", "honeypot") else 0.0
|
||||
elif val is None:
|
||||
val = 0.0
|
||||
else:
|
||||
try:
|
||||
val = float(val)
|
||||
except (ValueError, TypeError):
|
||||
val = 0.0
|
||||
|
||||
contrib = w * val
|
||||
log_days += contrib
|
||||
factor_contribs[k] = round(contrib, 3)
|
||||
|
||||
days = math.exp(log_days)
|
||||
|
||||
# Confidence based on data completeness
|
||||
present = sum(1 for k in WEIGHTS if features.get(k) not in (None, "", 0, False))
|
||||
confidence = min(1.0, present / max(len(WEIGHTS), 1))
|
||||
|
||||
# Clamp
|
||||
days = min(3650, max(0.1, days)) # 0.1 day to 10 years
|
||||
|
||||
return days, confidence, factor_contribs
|
||||
|
||||
|
||||
def _risk_level(days: float) -> str:
|
||||
if days < 1:
|
||||
return "critical"
|
||||
elif days < 7:
|
||||
return "high"
|
||||
elif days < 30:
|
||||
return "medium"
|
||||
return "low"
|
||||
|
||||
|
||||
@router.get("/predict/{chain}/{address}")
|
||||
async def predict_death_clock(
|
||||
chain: str,
|
||||
address: str,
|
||||
x_x402_sig: str | None = Query(None),
|
||||
):
|
||||
"""Predict days until rug/death for a token. Free: basic, Paid (x402): full model."""
|
||||
|
||||
# Fetch token data from SENTINEL scanner
|
||||
features: dict[str, Any] = {}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(
|
||||
f"{BACKEND}/api/v1/token/scan",
|
||||
json={"token_address": address, "chain": chain},
|
||||
headers={"X-RMI-Key": "rmi-internal-2026"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
free_data = data.get("free", {})
|
||||
features["liquidity_usd"] = free_data.get("liquidity_usd", 0)
|
||||
features["holder_count"] = free_data.get("holders", 0)
|
||||
features["lp_locked_pct"] = free_data.get("lp_locked_percent", 0)
|
||||
features["owner_renounced"] = free_data.get("owner_renounced", False)
|
||||
features["mint_authority"] = bool(free_data.get("mint_authority"))
|
||||
features["honeypot_risk"] = free_data.get("honeypot_risk", "")
|
||||
features["buy_tax"] = free_data.get("buy_tax", 0)
|
||||
features["sell_tax"] = free_data.get("sell_tax", 0)
|
||||
features["age_days"] = free_data.get("age_days", 0)
|
||||
features["volume_to_liquidity"] = (free_data.get("volume_24h", 0) or 0) / max(
|
||||
features["liquidity_usd"], 1
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(502, f"Scanner unavailable: {e}")
|
||||
|
||||
days, confidence, factors = _compute_death_clock(features)
|
||||
|
||||
result = {
|
||||
"token_address": address,
|
||||
"chain": chain,
|
||||
"symbol": features.get("symbol", "?"),
|
||||
"predicted_days": round(days, 1),
|
||||
"confidence": round(confidence, 2),
|
||||
"risk_level": _risk_level(days),
|
||||
"factors": factors,
|
||||
"explanation": _build_explanation(days, factors),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _build_explanation(days: float, factors: dict[str, float]) -> str:
|
||||
top = sorted(factors.items(), key=lambda x: abs(x[1]), reverse=True)[:3]
|
||||
parts = []
|
||||
for name, contrib in top:
|
||||
direction = "extends" if contrib < 0 else "shortens"
|
||||
parts.append(f"{name} ({'+' if contrib > 0 else ''}{contrib:.1f} log-days) {direction} lifespan")
|
||||
level = _risk_level(days)
|
||||
return f"Predicted {days:.1f} days ({level} risk). Key factors: {'; '.join(parts)}."
|
||||
Loading…
Add table
Add a link
Reference in a new issue