""" Premium x402 tools — reputation scoring, investigation reports, webhook alerts. These are the standout tools that make the RMI MCP server worth paying for: - reputation_score: Single 0-100 trust score combining all enrichment signals - investigation_report: AI-generated human-readable dossier - webhook_register: Register callbacks for monitoring alerts """ import json import logging import os import time from datetime import datetime import aiohttp from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field logger = logging.getLogger("x402.premium") router = APIRouter() # ═══════════════════════════════════════════════════════════ # Request Models # ═══════════════════════════════════════════════════════════ class AddressRequest(BaseModel): address: str = Field(..., description="Wallet address, token contract, or ENS name") chain: str = Field(default="base", description="Blockchain: base, solana, ethereum, bsc, etc.") class WebhookRegisterRequest(BaseModel): url: str = Field(..., description="Webhook callback URL") events: list[str] = Field(default=["rug_pull", "whale_move", "price_crash"], description="Events to monitor") address: str | None = Field(default=None, description="Specific address to watch (optional)") chain: str = Field(default="all", description="Chain filter") class InvestigationRequest(BaseModel): address: str = Field(..., description="Target address, token, or wallet") chain: str = Field(default="base") depth: str = Field(default="standard", description="standard | deep | forensic") # ═══════════════════════════════════════════════════════════ # TOOL: Reputation Score ($0.10) # ═══════════════════════════════════════════════════════════ @router.post("/reputation_score") async def reputation_score(req: AddressRequest): """Compute a comprehensive 0-100 trust score for any blockchain address. Combines: wallet labels, scam database hits, deployer history, RAG similarity matching, transaction patterns, and exchange associations. Score interpretation: 90-100: Trusted (verified exchange, known entity) 70-89: Low risk (established wallet, clean history) 50-69: Moderate risk (some flags, requires attention) 30-49: High risk (multiple red flags, suspected scam) 0-29: Critical risk (confirmed scam, sanctioned, known rugger) """ try: addr = req.address.strip() chain = req.chain or "base" score = 100 # Start at perfect trust, subtract for risk factors flags = [] positives = [] sources = [] # ── 1. Wallet Labels (ClickHouse) ── labels = await _lookup_labels_async(addr) if labels: sources.append("wallet_labels") for lbl in labels: cat = lbl.get("label_category", "") if cat == "sanctioned": score -= 100 flags.append( { "severity": "critical", "detail": f"OFAC-sanctioned: {lbl.get('label_name')}", } ) elif cat in ("phish-hack", "scam", "etherscan-phish-hack-list"): score -= 60 flags.append( { "severity": "high", "detail": f"Known scam: {lbl.get('label_name')} (source: {lbl.get('source')})", } ) elif cat == "exploit": score -= 50 flags.append( { "severity": "high", "detail": f"Exploit-associated: {lbl.get('label_name')}", } ) elif cat == "heist": score -= 70 flags.append( { "severity": "critical", "detail": f"Heist-associated: {lbl.get('label_name')}", } ) elif cat == "cex": score += 10 positives.append(f"Exchange wallet: {lbl.get('label_name')}") elif cat == "dex": score += 5 positives.append(f"DEX contract: {lbl.get('label_name')}") # ── 2. RAG Similarity Search ── try: from app.routers.x402_enrichment import search_similar_patterns patterns, _ = search_similar_patterns([addr]) if patterns: sources.append("rag_similarity") for p in patterns: matches = p.get("matches", []) for m in matches: sim_score = m.get("score", 0) if sim_score > 0.8: score -= 40 flags.append( { "severity": "high", "detail": f"Strong scam pattern match: {m.get('type')} ({sim_score:.0%})", } ) elif sim_score > 0.6: score -= 20 flags.append( { "severity": "medium", "detail": f"Possible scam pattern: {m.get('type')} ({sim_score:.0%})", } ) except Exception: pass # ── 3. Deployer History (DexScreener) ── try: async with aiohttp.ClientSession() as session: url = f"https://api.dexscreener.com/latest/dex/search?q={addr[:12]}" 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") # Age check oldest = min(pairs, key=lambda p: p.get("pairCreatedAt", 0)) age_h = ( (time.time() - oldest.get("pairCreatedAt", 0) / 1000) / 3600 if oldest.get("pairCreatedAt") else 0 ) if age_h < 1: score -= 15 flags.append( { "severity": "medium", "detail": f"Brand new: oldest pair only {age_h:.1f}h old", } ) elif age_h > 720: score += 10 positives.append(f"Established: oldest pair {age_h / 24:.0f}d old") # Volume/liquidity check total_liq = sum(p.get("liquidity", {}).get("usd", 0) for p in pairs) if total_liq > 0 and total_liq < 1000: score -= 10 flags.append( { "severity": "low", "detail": f"Low liquidity: ${total_liq:,.0f}", } ) except Exception: pass # ── 4. Scam Pattern Detection ── try: import asyncio from app.rag_service import detect_scam_patterns result = asyncio.run(detect_scam_patterns({"address": addr, "chain": chain}, 0.5)) if result and result.get("risk_score", 0) > 0: sources.append("scam_detector") risk = result.get("risk_score", 0) score -= min(risk, 50) patterns = result.get("patterns", []) if patterns: flags.append( { "severity": "high" if risk > 30 else "medium", "detail": f"Scam patterns: {', '.join(patterns[:3])}", } ) except Exception: pass # ── 5. Recent Intel Context ── try: from app.routers.x402_enrichment import _load_recent_intel intel = _load_recent_intel() if intel: sources.append("rmi_intel") positives.append( f"RMI Intel active: {intel.get('tokens_scanned', 0):,} tokens scanned, {intel.get('scanner_alerts', 0)} alerts" ) except Exception: pass # Clamp score score = max(0, min(100, score)) # If no sources had data, provide a baseline if not sources: score = 50 # Neutral — insufficient data flags.append( { "severity": "info", "detail": "Limited data available for this address. Score is neutral baseline.", } ) sources.append("baseline") # Determine tier if score >= 90: tier = "TRUSTED" elif score >= 70: tier = "LOW_RISK" elif score >= 50: tier = "MODERATE_RISK" elif score >= 30: tier = "HIGH_RISK" else: tier = "CRITICAL_RISK" return { "tool": "Reputation Score", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "address": addr, "chain": chain, "trust_score": score, "tier": tier, "flags": flags, "positive_signals": positives, "flag_count": len(flags), "sources_used": sources, "interpretation": { "90-100": "Trusted — verified exchange or known entity", "70-89": "Low risk — established wallet, clean history", "50-69": "Moderate risk — some flags, exercise caution", "30-49": "High risk — multiple red flags, suspected scam", "0-29": "Critical risk — confirmed scam, sanctioned, known rugger", }, "guarantee": "Data delivered or auto-refund via x402 receipt", } except Exception as e: logger.error(f"Reputation score failed: {e}") raise HTTPException(status_code=500, detail=str(e)) # ═══════════════════════════════════════════════════════════ # TOOL: Webhook Register ($0.02 setup + webhook delivery) # ═══════════════════════════════════════════════════════════ @router.post("/webhook_register") async def webhook_register(req: WebhookRegisterRequest): """Register a webhook URL for real-time monitoring alerts. Available events: rug_pull, whale_move, price_crash, new_launch, liquidity_removed, ownership_renounced, scam_detected. Webhooks fire within 30 seconds of detection. Max 3 webhooks per address. Data delivered as JSON POST to your URL. """ try: valid_events = { "rug_pull", "whale_move", "price_crash", "new_launch", "liquidity_removed", "ownership_renounced", "scam_detected", } events = [e for e in req.events if e in valid_events] if not events: raise HTTPException( status_code=400, detail=f"No valid events. Choose from: {', '.join(sorted(valid_events))}", ) # Store in 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=3, ) webhook_id = f"wh_{int(time.time())}_{req.address[:10] if req.address else 'global'}" webhook_data = { "id": webhook_id, "url": req.url, "events": events, "address": req.address, "chain": req.chain, "created": datetime.utcnow().isoformat(), "active": True, } r.setex(f"x402:webhook:{webhook_id}", 30 * 86400, json.dumps(webhook_data)) r.sadd(f"x402:webhook:events:{req.address or 'global'}", webhook_id) return { "tool": "Webhook Register", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "webhook_id": webhook_id, "url": req.url, "events": events, "expires_in": "30 days", "webhook_format": { "event": "e.g. rug_pull", "address": "0x...", "chain": "base", "data": "{tool-specific payload}", "timestamp": "ISO 8601", }, "guarantee": "Webhook delivery guaranteed or payment refunded", } except HTTPException: raise except Exception as e: logger.error(f"Webhook register failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/webhook_list") async def webhook_list(address: str | None = None): """List registered webhooks for an address or globally.""" try: 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, ) key = f"x402:webhook:events:{address or 'global'}" ids = r.smembers(key) webhooks = [] for wid in ids: data = r.get(f"x402:webhook:{wid}") if data: webhooks.append(json.loads(data)) return { "address": address or "global", "webhooks": webhooks, "count": len(webhooks), } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ═══════════════════════════════════════════════════════════ # TOOL: Investigation Report ($0.25) # ═══════════════════════════════════════════════════════════ @router.post("/investigation_report") async def investigation_report(req: InvestigationRequest): """Generate a comprehensive AI-powered investigation report. Combines: reputation scoring, wallet labeling, scam detection, on-chain forensics, social signal analysis, and market data into a single human-readable report with risk assessment. Report sections: Executive Summary, Risk Score, Wallet Profile, Transaction Analysis, Known Associations, Scam Indicators, Market Context, Recommendation. """ try: addr = req.address.strip() chain = req.chain or "base" depth = req.depth or "standard" sections = {} sources = [] # ── Section 1: Reputation ── try: async with ( aiohttp.ClientSession() as session, session.post( "http://localhost:8000/api/v1/x402-tools/reputation_score", json={"address": addr, "chain": chain}, timeout=aiohttp.ClientTimeout(total=15), ) as resp, ): if resp.status == 200: rep = await resp.json() sections["reputation"] = { "score": rep.get("trust_score"), "tier": rep.get("tier"), "flags": rep.get("flags", [])[:5], "positive_signals": rep.get("positive_signals", [])[:3], } sources.extend(rep.get("sources_used", [])) except Exception: sections["reputation"] = {"error": "Reputation service unavailable"} # ── Section 2: Wallet Labels ── labels = await _lookup_labels_async(addr) if labels: sources.append("wallet_labels") sections["wallet_profile"] = { "labels_found": len(labels), "categories": list({line.get("label_category") for line in labels}), "top_labels": [ { "name": line.get("label_name"), "category": line.get("label_category"), "source": line.get("source"), } for line in labels[:5] ], "sanctioned": any(line.get("label_category") == "sanctioned" for line in labels), } else: sections["wallet_profile"] = {"labels_found": 0, "note": "No known labels"} # ── Section 3: On-Chain Forensics ── try: async with ( aiohttp.ClientSession() as session, session.post( "http://localhost:8000/api/v1/x402-tools/forensics", json={"address": addr, "chain": chain}, timeout=aiohttp.ClientTimeout(total=20), ) as resp, ): if resp.status == 200: forensics = await resp.json() sections["on_chain"] = { "risk_score": forensics.get("overall_risk_score"), "risk_level": forensics.get("overall_risk"), "risk_factors": forensics.get("risk_factors", [])[:5], "sources": forensics.get("sources_used", []), } sources.extend(forensics.get("sources_used", [])) except Exception: sections["on_chain"] = {"error": "Forensics unavailable — continuing with other sources"} # ── Section 4: Market Context ── 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] sections["market_context"] = { "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"), "age_hours": (time.time() - p.get("pairCreatedAt", 0) / 1000) / 3600 if p.get("pairCreatedAt") else None, } except Exception: sections["market_context"] = {"error": "Market data unavailable"} # ── Section 5: Intel Context ── try: from app.routers.x402_enrichment import _load_recent_intel intel = _load_recent_intel() if intel: sections["intel_context"] = { "briefing": intel.get("briefing", "")[:300], "scanner_alerts": intel.get("scanner_alerts", 0), "age_hours": intel.get("age_hours", 0), } sources.append("rmi_cron_intel") except Exception: pass # ── Generate recommendation ── rep_score = sections.get("reputation", {}).get("score", 50) risk_score = sections.get("on_chain", {}).get("risk_score", 50) if rep_score >= 80 and risk_score < 30: verdict = "APPROVED" recommendation = "Low risk. Standard due diligence recommended." elif rep_score >= 60 and risk_score < 50: verdict = "CAUTION" recommendation = "Moderate risk. Verify contract ownership and liquidity locks before interacting." elif rep_score >= 40 or risk_score < 70: verdict = "HIGH_RISK" recommendation = "High risk. Multiple red flags detected. Exercise extreme caution or avoid." else: verdict = "AVOID" recommendation = "Critical risk. Confirmed scam indicators. Do not interact with this address." return { "tool": "Investigation Report", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "address": addr, "chain": chain, "depth": depth, "verdict": verdict, "recommendation": recommendation, "sections": sections, "sources_used": list(set(sources)), "source_count": len(set(sources)), "price_usd": "0.25", "guarantee": "Comprehensive report or full refund", } except HTTPException: raise except Exception as e: logger.error(f"Investigation report failed: {e}") raise HTTPException(status_code=500, detail=str(e)) # ═══════════════════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════════════════ async def _lookup_labels_async(address: str) -> list[dict]: """Async wrapper for wallet label lookups.""" try: 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, ) # Try ClickHouse first try: from clickhouse_driver import Client ch = Client( host=os.getenv("CH_HOST", "rmi-clickhouse"), port=int(os.getenv("CH_PORT", "9000")), user=os.getenv("CH_USER", "default"), password=os.getenv("CH_PASSWORD", "") or None, settings={"max_execution_time": 3}, ) rows = ch.execute( "SELECT address, label_name, label_category, label_subtype, source, is_sanctioned " "FROM wallet_memory.wallet_labels WHERE address = %(addr)s " "ORDER BY loaded_at DESC LIMIT 20", {"addr": address}, ) return [ { "label_name": r[1], "label_category": r[2], "label_subtype": r[3], "source": r[4], "is_sanctioned": bool(r[5]), } for r in rows ] except Exception: # Fall back to Redis cache cached = r.get(f"x402:enrich:{address}") if cached: data = json.loads(cached) return data.get("labels", []) return [] except Exception: return []