rmi-backend/app/routers/x402_advanced_tools.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

902 lines
36 KiB
Python

"""
Advanced x402 tools - caching, confidence, real-time streams, predictive scoring.
Layer 1: Response caching (Redis, 60s TTL) - <50ms repeat calls
Layer 2: Confidence scores - every data point gets low/medium/high
Layer 3: SSE alert stream - real-time rug/whale/price alerts
Layer 4: Rug probability - predictive 0-100 "will this rug in 24h?"
Layer 5: Historical scanner data - time-series risk/liquidity/holders
Layer 6: Narrative engine - what's the market saying RIGHT NOW
"""
import asyncio
import hashlib
import json
import logging
import os
import time
from datetime import datetime
import aiohttp
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
logger = logging.getLogger("x402.advanced")
router = APIRouter()
# ═══════════════════════════════════════════════════════════
# LAYER 1: Response Caching
# ═══════════════════════════════════════════════════════════
CACHE_TTL = 60 # seconds
CACHEABLE_TOOLS = {
"audit",
"wallet",
"reputation_score",
"honeypot_check",
"rugshield",
"forensics",
"whale",
"token_deep_dive",
"market_overview",
"chain_health",
"sentiment",
"rug_pull_predictor",
"rug_probability",
"narrative",
}
def _redis_conn():
import redis as _redis
return _redis.Redis(
host=os.getenv("REDIS_HOST", "rmi-redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2,
)
def get_cached(tool: str, params: dict) -> dict | None:
"""Check Redis cache for a tool result."""
try:
r = _redis_conn()
key = _cache_key(tool, params)
data = r.get(key)
if data:
return json.loads(data)
except Exception:
pass
return None
def set_cached(tool: str, params: dict, result: dict, ttl: int = CACHE_TTL):
"""Store tool result in Redis cache."""
try:
r = _redis_conn()
key = _cache_key(tool, params)
result["_cached"] = True
result["_cached_at"] = datetime.utcnow().isoformat()
r.setex(key, ttl, json.dumps(result))
except Exception:
pass
def _cache_key(tool: str, params: dict) -> str:
raw = f"{tool}:{json.dumps(params, sort_keys=True)}"
return f"x402:cache:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
def invalidate_cache(tool: str | None = None):
"""Clear cache for a tool or all cached results."""
try:
r = _redis_conn()
if tool:
for key in r.scan_iter("x402:cache:*"):
r.delete(key)
else:
for key in r.scan_iter("x402:cache:*"):
r.delete(key)
return True
except Exception:
return False
# ═══════════════════════════════════════════════════════════
# LAYER 2: Confidence Scores
# ═══════════════════════════════════════════════════════════
def compute_confidence(result: dict) -> dict:
"""Add confidence scoring to any tool result."""
sources = result.get("sources_used", [])
source_count = len(sources)
# Source quality weights
high_quality = {
"clickhouse",
"wallet_labels",
"etherscan",
"coingecko",
"defillama",
"solana_rpc",
"ethereum_rpc",
"base_rpc",
"dexscreener",
"geckoterminal",
"tron_rpc",
"bitcoin_rpc",
}
medium_quality = {
"cryptopanic",
"reddit",
"coincap",
"coinmarketcap",
"moralis",
"birdeye",
"helius",
"solscan",
"rmi_intel",
"scam_detector",
"rag_similarity",
"pumpfun",
}
high_count = sum(1 for s in sources if s.lower() in high_quality)
medium_count = sum(1 for s in sources if s.lower() in medium_quality)
source_count - high_count - medium_count
# Score computation
if source_count >= 4 and high_count >= 2:
level = "high"
score = min(100, 70 + source_count * 5)
elif source_count >= 2 and high_count >= 1:
level = "medium"
score = 40 + source_count * 8
elif source_count >= 1:
level = "low"
score = 20 + source_count * 10
else:
level = "unverified"
score = 5
# Flag freshness
age_flags = []
if "dexscreener" in [s.lower() for s in sources]:
age_flags.append("market_data_live")
if "wallet_labels" in [s.lower() for s in sources]:
age_flags.append("labels_cached")
return {
"score": min(100, score),
"level": level,
"sources_total": source_count,
"sources_high_quality": high_count,
"sources_medium_quality": medium_count,
"flags": age_flags,
"interpretation": {
"high": "Multiple verified sources confirm this data",
"medium": "Adequate coverage from trusted sources",
"low": "Limited source coverage - verify independently",
"unverified": "Insufficient data - treat as directional only",
}.get(level, ""),
}
# ═══════════════════════════════════════════════════════════
# LAYER 3: SSE Real-Time Alert Stream
# ═══════════════════════════════════════════════════════════
@router.get("/stream/alerts")
async def sse_alert_stream(
events: str = Query(default="rug_pull,whale_move,price_crash", description="Comma-separated event types"),
chain: str = Query(default="all"),
):
"""Server-Sent Events stream for real-time security alerts.
Connect with: EventSource('/api/v1/x402-tools/stream/alerts?events=rug_pull,whale_move')
Events emitted as JSON:
{"event":"rug_pull","address":"0x...","chain":"base","severity":"critical","data":{...}}
"""
event_list = [e.strip() for e in events.split(",") if e.strip()]
chain_filter = chain.strip().lower()
async def event_generator():
import redis as _redis
r = _redis.Redis(
host=os.getenv("REDIS_HOST", "rmi-redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
socket_connect_timeout=3,
)
last_id = "0"
# Send initial connection event
yield f"event: connected\ndata: {json.dumps({'status': 'connected', 'events': event_list, 'chain': chain_filter, 'timestamp': datetime.utcnow().isoformat()})}\n\n"
while True:
try:
# Check Redis for new alerts
alerts = r.lrange("x402:alerts:high_risk", 0, 9)
for alert_json in alerts:
try:
alert = json.loads(alert_json)
alert_id = alert.get("timestamp", "")
if alert_id > last_id:
event_type = alert.get("type", "unknown")
if event_type in event_list or "all" in event_list:
yield f"event: {event_type}\ndata: {json.dumps(alert)}\n\n"
last_id = alert_id
except json.JSONDecodeError:
continue
# Also check for new events in webhook-triggered alerts
for evt in event_list:
evt_alerts = r.lrange(f"x402:alert:{evt}", 0, 4)
for a in evt_alerts:
try:
data = json.loads(a)
if data.get("timestamp", "") > last_id:
yield f"event: {evt}\ndata: {json.dumps(data)}\n\n"
last_id = data.get("timestamp", "")
except json.JSONDecodeError:
continue
# Keep-alive ping
yield f": keepalive {int(time.time())}\n\n"
await asyncio.sleep(5)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"SSE stream error: {e}")
await asyncio.sleep(10)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
"Access-Control-Allow-Origin": "*",
},
)
# ═══════════════════════════════════════════════════════════
# Request Models
# ═══════════════════════════════════════════════════════════
class AddressRequest(BaseModel):
address: str = Field(..., description="Wallet or token address")
chain: str = Field(default="base")
class HistoryRequest(BaseModel):
address: str = Field(..., description="Token or wallet address")
chain: str = Field(default="base")
hours: int = Field(default=24, ge=1, le=168, description="Lookback window in hours (max 168)")
class NarrativeRequest(BaseModel):
token: str = Field(..., description="Token symbol or address")
chain: str = Field(default="all")
# ═══════════════════════════════════════════════════════════
# LAYER 4: Rug Probability Score
# ═══════════════════════════════════════════════════════════
@router.post("/rug_probability")
async def rug_probability(req: AddressRequest):
"""Predictive rug pull probability: 0-100 score for "will this token rug in 24h?"
Combines 7 signals:
- Honeypot check (can you sell?)
- Liquidity depth and lock status
- Holder concentration (whale dominance)
- Deployer history (serial rugger?)
- Contract age (new = higher risk)
- Social signal anomalies (coordinated shilling)
- Market context (volume/liquidity ratio)
"""
try:
addr = req.address.strip()
chain = req.chain or "base"
t0 = time.time()
# Check cache
cached = get_cached("rug_probability", {"address": addr, "chain": chain})
if cached:
return cached
probability = 0
signals = []
sources = []
# ── Signal 1: Honeypot Check ──
try:
async with aiohttp.ClientSession() as session, session.post(
"http://localhost:8000/api/v1/x402-tools/honeypot_check",
json={"address": addr, "chain": chain},
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.status == 200:
data = await resp.json()
if data.get("is_honeypot"):
probability += 40
signals.append(
{
"signal": "honeypot_detected",
"weight": 40,
"detail": data.get("reason", "Cannot sell - confirmed honeypot"),
}
)
sources.append("honeypot_check")
except Exception:
pass
# ── Signal 2: DexScreener Liquidity & Age ──
try:
async with aiohttp.ClientSession() as session:
url = f"https://api.dexscreener.com/latest/dex/tokens/{addr}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
data = await resp.json()
pairs = data.get("pairs", [])
if pairs:
sources.append("dexscreener")
p = pairs[0]
liq = p.get("liquidity", {}).get("usd", 0) or 0
vol = p.get("volume", {}).get("h24", 0) or 0
age_ms = p.get("pairCreatedAt", 0) or 0
age_h = (time.time() * 1000 - age_ms) / 3600000 if age_ms else 0
# Liquidity signal
if liq < 1000:
probability += 25
signals.append(
{
"signal": "critical_low_liquidity",
"weight": 25,
"detail": f"Liquidity ${liq:,.0f} - extremely low, easy to drain",
}
)
elif liq < 10000:
probability += 15
signals.append(
{
"signal": "low_liquidity",
"weight": 15,
"detail": f"Liquidity ${liq:,.0f} - below safe threshold",
}
)
elif liq < 50000:
probability += 5
signals.append(
{
"signal": "moderate_liquidity",
"weight": 5,
"detail": f"Liquidity ${liq:,.0f} - moderate",
}
)
# Age signal
if 0 < age_h < 1:
probability += 20
signals.append(
{
"signal": "brand_new",
"weight": 20,
"detail": f"Only {age_h:.1f}h old - highest rug risk window",
}
)
elif 0 < age_h < 6:
probability += 12
signals.append(
{
"signal": "very_new",
"weight": 12,
"detail": f"Only {age_h:.1f}h old - early risk period",
}
)
elif 0 < age_h < 24:
probability += 5
signals.append(
{
"signal": "new_token",
"weight": 5,
"detail": f"{age_h:.0f}h old - still in risk window",
}
)
# Volume/Liquidity ratio (pump and dump signal)
if liq > 0 and vol > liq * 3:
probability += 10
signals.append(
{
"signal": "pump_dump_pattern",
"weight": 10,
"detail": f"Volume {vol / liq:.0f}x liquidity - possible pump and dump",
}
)
# Price crash signal
pc = p.get("priceChange", {}).get("h24", 0) or 0
if pc < -50:
probability += 15
signals.append(
{
"signal": "price_crashing",
"weight": 15,
"detail": f"Down {pc:.0f}% in 24h - possible exit scam in progress",
}
)
elif pc < -20:
probability += 8
signals.append(
{
"signal": "price_declining",
"weight": 8,
"detail": f"Down {pc:.0f}% in 24h",
}
)
except Exception:
pass
# ── Signal 3: Holder Concentration ──
try:
async with aiohttp.ClientSession() as session:
# GeckoTerminal for holder data
chain_map = {"solana": "solana", "base": "base", "ethereum": "eth", "bsc": "bsc"}
geo_chain = chain_map.get(chain, chain)
url = f"https://api.geckoterminal.com/api/v2/networks/{geo_chain}/tokens/{addr}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
data = await resp.json()
attrs = data.get("data", {}).get("attributes", {})
if attrs:
sources.append("geckoterminal")
# Check top holder concentration from available data
top_pool = attrs.get("top_pool_id")
if top_pool:
# Pool exists - check if it's the only one
pass
except Exception:
pass
# ── Signal 4: Deployer History (scam pattern check) ──
try:
import asyncio as _asyncio
from app.rag_service import detect_scam_patterns
result = _asyncio.run(detect_scam_patterns({"address": addr, "chain": chain}, 0.4))
if result and result.get("risk_score", 0) > 0:
sources.append("scam_detector")
risk = result.get("risk_score", 0)
probability += min(risk, 30)
patterns = result.get("patterns", [])
if patterns:
signals.append(
{
"signal": "scam_pattern_match",
"weight": min(risk, 30),
"detail": f"Matches known scam patterns: {', '.join(patterns[:3])}",
}
)
except Exception:
pass
# ── Signal 5: Social Anomaly Check ──
try:
async with aiohttp.ClientSession() as session:
from urllib.parse import quote
symbol = addr[:12]
url = f"https://cryptopanic.com/api/free/posts/?filter=important&q={quote(symbol)}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
data = await resp.json()
posts = data.get("results", [])
if posts:
sources.append("cryptopanic")
# Check for sudden spike in mentions
recent = [p for p in posts if p.get("created_at")]
if len(recent) > 20:
probability += 5
signals.append(
{
"signal": "social_spike",
"weight": 5,
"detail": f"{len(recent)} social mentions - unusual activity",
}
)
except Exception:
pass
# ── Compute final probability ──
probability = max(0, min(100, probability))
if probability >= 75:
tier = "EXTREME_RISK"
recommendation = "DO NOT BUY - extremely high rug probability"
elif probability >= 50:
tier = "HIGH_RISK"
recommendation = "Avoid - significant rug indicators present"
elif probability >= 25:
tier = "MODERATE_RISK"
recommendation = "Caution - monitor closely before entry"
elif probability >= 10:
tier = "LOW_RISK"
recommendation = "Standard risk - normal market activity"
else:
tier = "MINIMAL_RISK"
recommendation = "Low rug probability - relatively safe"
result = {
"tool": "Rug Probability Score",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"address": addr,
"chain": chain,
"rug_probability": probability,
"tier": tier,
"recommendation": recommendation,
"signals": signals,
"signal_count": len(signals),
"sources_used": sources,
"_confidence": compute_confidence({"sources_used": sources}),
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Data delivered or auto-refund via x402 receipt",
}
set_cached("rug_probability", {"address": addr, "chain": chain}, result)
return result
except Exception as e:
logger.error(f"Rug probability failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# LAYER 5: Historical Scanner Data
# ═══════════════════════════════════════════════════════════
@router.post("/history")
async def token_history(req: HistoryRequest):
"""Historical risk/liquidity/holder data for any token.
Returns time-series data from the RMI scanner (runs every 10 min).
Shows how risk profile, liquidity, volume, and holder metrics change over time.
Data points: timestamp, risk_score, liquidity_usd, volume_24h,
price_usd, holder_count, whale_dominance_pct.
"""
try:
addr = req.address.strip()
chain = req.chain or "base"
hours = req.hours
cached = get_cached("history", {"address": addr, "chain": chain, "hours": hours})
if cached:
return cached
# Read scanner snapshots from Redis
import redis as _redis
r = _redis.Redis(
host=os.getenv("REDIS_HOST", "rmi-redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD", ""),
decode_responses=True,
socket_connect_timeout=2,
)
data_points = []
cutoff = time.time() - (hours * 3600)
# Scanner stores data as x402:scan:{address}:{timestamp}
for key in r.scan_iter(f"x402:scan:{addr}:*"):
try:
ts = float(key.decode().split(":")[-1]) if isinstance(key, bytes) else float(key.split(":")[-1])
if ts < cutoff:
continue
raw = r.get(key)
if raw:
dp = json.loads(raw)
dp["timestamp"] = ts
data_points.append(dp)
except (ValueError, json.JSONDecodeError):
continue
# Also try the token scanner's own keys
for key in r.scan_iter(f"token:scan:{addr}:*"):
try:
parts = key.decode().split(":") if isinstance(key, bytes) else key.split(":")
ts = float(parts[-1])
if ts < cutoff:
continue
raw = r.get(key)
if raw:
dp = json.loads(raw)
dp["timestamp"] = ts
data_points.append(dp)
except (ValueError, json.JSONDecodeError):
continue
data_points.sort(key=lambda d: d.get("timestamp", 0))
# Add current snapshot
try:
async with aiohttp.ClientSession() as session:
url = f"https://api.dexscreener.com/latest/dex/tokens/{addr}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
dex_data = await resp.json()
pairs = dex_data.get("pairs", [])
if pairs:
p = pairs[0]
current = {
"timestamp": time.time(),
"price_usd": p.get("priceUsd"),
"liquidity_usd": p.get("liquidity", {}).get("usd"),
"volume_24h": p.get("volume", {}).get("h24"),
"price_change_24h": p.get("priceChange", {}).get("h24"),
"is_current": True,
}
data_points.append(current)
except Exception:
pass
# Compute trends
trends = {}
if len(data_points) >= 2:
first = data_points[0]
last = data_points[-1]
for metric in ["price_usd", "liquidity_usd", "volume_24h"]:
fv = first.get(metric)
lv = last.get(metric)
if fv and lv and fv > 0:
change_pct = ((lv - fv) / fv) * 100
trends[metric] = {
"start": fv,
"end": lv,
"change_pct": round(change_pct, 1),
"direction": "up" if change_pct > 0 else "down" if change_pct < 0 else "flat",
}
result = {
"tool": "Historical Scanner Data",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"address": addr,
"chain": chain,
"lookback_hours": hours,
"data_points": len(data_points),
"history": data_points[-50:], # Last 50 data points
"trends": trends,
"scanner_interval": "10 minutes",
"_confidence": compute_confidence(
{
"sources_used": ["rmi_scanner"]
+ (["dexscreener"] if any(d.get("is_current") for d in data_points) else []),
}
),
"guarantee": "Historical data or full refund",
}
set_cached("history", {"address": addr, "chain": chain, "hours": hours}, result, ttl=120)
return result
except Exception as e:
logger.error(f"Token history failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# LAYER 6: Narrative Engine
# ═══════════════════════════════════════════════════════════
@router.post("/narrative")
async def narrative_engine(req: NarrativeRequest):
"""What's the market saying about this token RIGHT NOW?
Aggregates Twitter, Reddit, Telegram, and news sentiment into a
narrative summary with confidence scoring and shill detection.
"""
try:
token = req.token.strip()
chain = req.chain or "all"
t0 = time.time()
cached = get_cached("narrative", {"token": token, "chain": chain})
if cached:
return cached
sources = []
posts = []
# ── CryptoPanic (news + social) ──
try:
from urllib.parse import quote
async with aiohttp.ClientSession() as session:
url = f"https://cryptopanic.com/api/free/posts/?filter=important&q={quote(token)}"
async with session.get(url, timeout=aiohttp.ClientTimeout(total=8)) as resp:
if resp.status == 200:
data = await resp.json()
results = data.get("results", [])
if results:
sources.append("cryptopanic")
for r in results[:15]:
posts.append(
{
"source": "cryptopanic",
"title": r.get("title", "")[:150],
"sentiment": r.get("votes", {}).get("positive", 0)
- r.get("votes", {}).get("negative", 0),
"created": r.get("created_at", ""),
}
)
except Exception:
pass
# ── Reddit ──
try:
from urllib.parse import quote
async with aiohttp.ClientSession() as session:
url = f"https://www.reddit.com/search.json?q={quote(token)}&sort=new&limit=10"
async with session.get(
url,
headers={"User-Agent": "RMI-Narrative/1.0"},
timeout=aiohttp.ClientTimeout(total=8),
) as resp:
if resp.status == 200:
data = await resp.json()
children = data.get("data", {}).get("children", [])
if children:
sources.append("reddit")
for c in children[:10]:
d = c.get("data", {})
posts.append(
{
"source": "reddit",
"title": d.get("title", "")[:150],
"subreddit": d.get("subreddit", ""),
"score": d.get("score", 0),
"comments": d.get("num_comments", 0),
"created": datetime.utcfromtimestamp(d.get("created_utc", 0)).isoformat()
if d.get("created_utc")
else "",
}
)
except Exception:
pass
# ── Compute narrative ──
total_posts = len(posts)
if total_posts == 0:
return {
"tool": "Narrative Engine",
"version": "1.0",
"address": token,
"narrative": "Insufficient data - no recent mentions found",
"sentiment": "neutral",
"confidence": "low",
"posts_analyzed": 0,
"sources_used": [],
}
# Sentiment scoring
positive = sum(1 for p in posts if p.get("sentiment", 0) > 0)
negative = sum(1 for p in posts if p.get("sentiment", 0) < 0)
neutral_count = total_posts - positive - negative
if positive > negative * 2:
sentiment = "bullish"
sentiment_pct = round(positive / max(total_posts, 1) * 100)
elif negative > positive * 2:
sentiment = "bearish"
sentiment_pct = round(negative / max(total_posts, 1) * 100)
elif positive > negative:
sentiment = "slightly_bullish"
sentiment_pct = round(positive / max(total_posts, 1) * 100)
elif negative > positive:
sentiment = "slightly_bearish"
sentiment_pct = round(negative / max(total_posts, 1) * 100)
else:
sentiment = "neutral"
sentiment_pct = 50
# Shill detection
shill_signals = []
reddit_posts = [p for p in posts if p.get("source") == "reddit"]
if total_posts > 10 and positive > total_posts * 0.8:
shill_signals.append("Suspiciously high positive ratio - possible coordinated shilling")
if len(reddit_posts) >= 3 and all(p.get("score", 0) == 0 for p in reddit_posts):
shill_signals.append("Same-timestamp posts detected - possible bot activity")
# Build narrative summary
subreddits = list({p.get("subreddit", "") for p in posts if p.get("subreddit")})
narrative = (
f"{sentiment.replace('_', ' ').title()} on {token}. "
f"{sentiment_pct}% positive across {total_posts} posts from {len(sources)} sources. "
+ (f"Active in r/{', r/'.join(subreddits[:3])}. " if subreddits else "")
+ (f"Risk: {'; '.join(shill_signals)}" if shill_signals else "No shill signals detected.")
)
result = {
"tool": "Narrative Engine",
"version": "1.0",
"timestamp": datetime.utcnow().isoformat(),
"token": token,
"chain": chain,
"narrative": narrative,
"sentiment": sentiment,
"sentiment_score": sentiment_pct,
"posts_analyzed": total_posts,
"breakdown": {
"positive": positive,
"negative": negative,
"neutral": neutral_count,
},
"sources_used": sources,
"shill_signals": shill_signals if shill_signals else None,
"_confidence": compute_confidence({"sources_used": sources}),
"performance_ms": round((time.time() - t0) * 1000, 1),
"guarantee": "Real-time social data or full refund",
}
set_cached("narrative", {"token": token, "chain": chain}, result)
return result
except Exception as e:
logger.error(f"Narrative engine failed: {e}")
raise HTTPException(status_code=500, detail=str(e)) from e
# ═══════════════════════════════════════════════════════════
# Cache Management Endpoint
# ═══════════════════════════════════════════════════════════
@router.post("/cache/clear")
async def clear_cache(tool: str = Query(default=None)):
"""Clear response cache for a specific tool or all tools."""
ok = invalidate_cache(tool)
return {
"status": "cleared" if ok else "failed",
"tool": tool or "all",
"timestamp": datetime.utcnow().isoformat(),
}
@router.get("/cache/stats")
async def cache_stats():
"""Get cache hit/miss statistics."""
try:
r = _redis_conn()
keys = list(r.scan_iter("x402:cache:*"))
return {
"cached_entries": len(keys),
"memory_estimate_bytes": sum(len(r.get(k) or "") for k in keys[:100]),
"timestamp": datetime.utcnow().isoformat(),
}
except Exception as e:
return {"error": str(e), "cached_entries": 0}