#!/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)}."