rmi-backend/app/routers/x402_enrichment.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

848 lines
26 KiB
Python

"""
x402 Tool Response Enrichment Middleware
═══════════════════════════════════════════════════════════
Post-execution enrichment layer that annotates x402 tool responses
with wallet intelligence from the Memory Bank (393K labels, 155K addresses)
and live Etherscan V2 contract name lookups.
Architecture:
1. Tool executes → raw result
2. Extract addresses from result
3. Batch ClickHouse lookup (label_name, label_category, is_sanctioned)
4. Live Etherscan V2 contract name lookups for unknown addresses
5. If high-value: RAG similarity search against known scam patterns
6. Optionally: LLM-synthesize a human-readable enrichment paragraph
7. Merge enrichment into response JSON
Caching: Redis x402:enrich:{addr} with 1hr TTL.
Opt-out: ?enrich=false in query params.
Always opt-in for premium tools, opt-out for speed-only callers.
Storage impact: ~500 Redis keys at steady state, 0 ClickHouse writes.
"""
import datetime
import json
import logging
import time
from dataclasses import dataclass, field
from typing import Any
logger = logging.getLogger("x402.enrich")
# ── Which tool categories get enrichment ────────────────────────
# Wallet-centric tools: enrich with labels, scam checking
# Token-centric tools: enrich with holder label analysis
# Market tools: skip (low overlap, not worth the lookup)
ENRICHABLE_CATEGORIES = {
"wallet": [
"whale",
"smartmoney",
"cluster",
"insider",
"copy_trade_finder",
"wallet",
"portfolio_tracker",
"portfolio_aggregate",
"wallet_pnl",
"smart_money_alpha",
"cross_chain_whale",
"dormant_whale_alert",
"full_wallet_dossier",
"wallet_cluster_score",
"wallet_drain_scanner",
"wallet_label_registry",
"whale_network_map",
"whale_profile",
"whale_scan",
"whale_accumulation",
"sniper_detect",
"syndicate_scan",
"syndicate_track",
"wallet_graph",
"deployer_history",
"kol_performance",
"insider_network",
"airdrop_finder",
],
"token": [
"audit",
"rugshield",
"forensics",
"comprehensive_audit",
"honeypot_check",
"rug_pull_predictor",
"token_deep_dive",
"token_comparison",
"fresh_pair",
"clone_detect",
"launch",
"launch_intel",
"sniper_alert",
"token_age",
"token_distribution_health",
"token_velocity",
"token_watch_create",
"token_watch_check",
"token_watch_alerts",
"token_watch_list",
"presale_scanner",
"fair_launch_detect",
"listing_predictor",
"arbitrage_scan",
"scam_database",
"protocol_risk",
"liquidity_depth",
"liquidity_migration",
"wash_trading",
"bundler_detect",
"mev_alert",
"mev_protection",
"liquidity_flow",
],
"security": [
"urlcheck",
"sentiment",
"social_signal",
"anomaly",
"profile_flip",
"contract_info",
"contract_clone_check",
"slither_audit",
"storage_reader",
"tx_decoder",
"reentrancy_scanner",
"phantom_mint_detect",
"tw_profile",
"tw_timeline",
"tw_search",
"bridge_security",
"nft_wash_detector",
"risk_monitor",
"pulse",
"fresh_pair",
"sniper_alert",
],
"market": [
"market_overview",
"chain_health",
"defi_yield_scanner",
"gas_forecast",
"anomaly_detector",
"launch_intel",
],
}
# Per-chain variants - strip _chain suffix and check base tool name
CHAIN_SUFFIXES = {
"solana",
"base",
"ethereum",
"bsc",
"polygon",
"arbitrum",
"optimism",
"avalanche",
"fantom",
"gnosis",
"tron",
"bitcoin",
}
def _is_enrichable(tool_name: str) -> str:
"""Check if tool is enrichable, handling per-chain variants."""
if "_" in tool_name:
parts = tool_name.rsplit("_", 1)
if len(parts) == 2 and parts[1] in CHAIN_SUFFIXES:
base_name = parts[0]
for cat, tools in ENRICHABLE_CATEGORIES.items():
if base_name in tools:
return cat
for cat, tools in ENRICHABLE_CATEGORIES.items():
if tool_name in tools:
return cat
return ""
MAX_BATCH_SIZE = 200 # Max addresses per ClickHouse batch query
ENRICH_CACHE_TTL = 3600 # 1 hour
SIMILARITY_THRESHOLD = 0.6 # Cosine similarity floor for RAG matches
@dataclass
class WalletLabel:
"""Lightweight label from ClickHouse lookup."""
address: str
label_name: str = ""
label_category: str = ""
label_subtype: str = ""
source: str = ""
is_sanctioned: bool = False
@dataclass
class EnrichmentResult:
"""Merged enrichment for all addresses in a tool response."""
labels: dict[str, list[WalletLabel]] = field(default_factory=dict)
sanctioned: list[str] = field(default_factory=list)
risk_summary: str = ""
similar_patterns: list[dict] = field(default_factory=list)
lookup_ms: float = 0
rag_ms: float = 0
def _redis(enrich_cache: dict | None = None):
"""Lazy Redis connection (sync client)."""
import os
import redis as _redis
from dotenv import load_dotenv
load_dotenv(override=True)
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 _ch():
"""Lazy ClickHouse connection."""
import os
from clickhouse_driver import Client
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},
)
def extract_addresses(result: dict[str, Any], tool_name: str) -> list[str]:
"""
Extract wallet/token addresses from a tool result for enrichment lookup.
Handles common response patterns across all x402 tools.
"""
addresses = set()
if not isinstance(result, dict):
return []
# Direct fields
for key in (
"address",
"wallet",
"token_address",
"contract_address",
"pair_address",
"creator",
"owner",
"payTo",
):
val = result.get(key)
if isinstance(val, str) and len(val) >= 32:
addresses.add(val)
# Nested lists (holder breakdown, top wallets, cluster members)
for list_key in (
"holders",
"top_wallets",
"top_holders",
"cluster_members",
"whale_wallets",
"suspicious_wallets",
"linked_addresses",
"audits",
"results",
):
items = result.get(list_key, [])
if isinstance(items, list):
for item in items:
if isinstance(item, dict):
addr = item.get("address") or item.get("wallet") or item.get("token_address")
if addr and isinstance(addr, str) and len(addr) >= 32:
addresses.add(addr)
elif isinstance(item, str) and len(item) >= 32:
addresses.add(item)
# Token comparisons have numbered entries
for key, val in result.items(): # noqa: B007
if isinstance(val, dict):
addr = val.get("address") or val.get("token_address")
if addr and isinstance(addr, str) and len(addr) >= 32:
addresses.add(addr)
return list(addresses)[:MAX_BATCH_SIZE]
def lookup_labels(addresses: list[str]) -> tuple[dict[str, list[WalletLabel]], float]:
"""
Batch lookup wallet labels from ClickHouse.
Returns: {address: [WalletLabel, ...]}, query_time_ms
"""
if not addresses:
return {}, 0
t0 = time.time()
results: dict[str, list[WalletLabel]] = {a: [] for a in addresses}
try:
ch = _ch()
# Use IN clause for batch lookup
placeholders = ", ".join(["(a)" for a in addresses])
query = f"""
SELECT address, label_name, label_category, label_subtype, source, is_sanctioned
FROM wallet_memory.wallet_labels
WHERE address IN ({placeholders})
ORDER BY
CASE label_category
WHEN 'sanctioned' THEN 0
WHEN 'phish-hack' THEN 1
WHEN 'scam' THEN 2
WHEN 'etherscan-phish-hack-list' THEN 3
WHEN 'exploit' THEN 4
WHEN 'heist' THEN 5
ELSE 99
END,
loaded_at DESC
LIMIT {MAX_BATCH_SIZE * 5}
"""
rows = ch.execute(query, addresses)
for row in rows:
addr = row[0]
label = WalletLabel(
address=addr,
label_name=row[1] or "",
label_category=row[2] or "",
label_subtype=row[3] or "",
source=row[4] or "",
is_sanctioned=bool(row[5]),
)
results[addr].append(label)
except Exception as e:
logger.warning(f"CH label lookup failed: {e}")
return {}, (time.time() - t0) * 1000
return results, (time.time() - t0) * 1000
def search_similar_patterns(
addresses: list[str],
r: Any = None,
) -> tuple[list[dict], float]:
"""
Search for similar scam/wallet patterns using RAG embeddings.
Falls back gracefully if RAG is unavailable.
"""
t0 = time.time()
patterns = []
try:
from app.rag_service import get_embedder, search_multi_collection
embedder = get_embedder()
if not embedder:
return [], 0
for addr in addresses[:10]: # Max 10 RAG queries per call
# Check cache first
cache_key = f"x402:enrich:rag:{addr}"
if r:
cached = r.get(cache_key)
if cached:
patterns.append(json.loads(cached))
continue
try:
# Generate embedding for this address
emb = embedder.embed(addr)
if not emb or not emb.vector:
continue
# Multi-collection search
hits = search_multi_collection(
emb.vector,
collections=["wallet_profiles", "known_scams"],
limit=3,
min_score=SIMILARITY_THRESHOLD,
)
if hits:
pattern = {
"address": addr,
"matches": [
{
"collection": h.get("collection", ""),
"score": round(h.get("score", 0), 3),
"type": h.get("metadata", {}).get("type", ""),
"description": h.get("metadata", {}).get("description", "")[:200],
}
for h in hits[:2]
],
}
patterns.append(pattern)
if r:
r.setex(cache_key, ENRICH_CACHE_TTL, json.dumps(pattern))
except Exception as e:
logger.debug(f"RAG search failed for {addr[:10]}: {e}")
continue
except Exception as e:
logger.debug(f"RAG enrichment unavailable: {e}")
return patterns, (time.time() - t0) * 1000
def build_risk_summary(
labels: dict[str, list[WalletLabel]],
sanctioned: list[str],
similar_patterns: list[dict],
) -> str:
"""Build a concise human-readable risk summary."""
parts = []
total_addrs = len(labels)
labeled = sum(1 for v in labels.values() if v)
if labeled:
parts.append(f"{labeled}/{total_addrs} addresses have known labels")
if sanctioned:
parts.append(f"{len(sanctioned)} OFAC-sanctioned wallet(s)")
scam_count = sum(
1 for v in labels.values() for line in v if line.label_category in ("phish-hack", "scam", "etherscan-phish-hack-list")
)
if scam_count:
parts.append(f"{scam_count} known phishing/scam address(es) detected")
cex_count = sum(1 for v in labels.values() for line in v if line.label_category == "cex")
if cex_count:
parts.append(f"{cex_count} exchange wallet(s) identified")
if similar_patterns:
high_matches = [p for p in similar_patterns if p.get("matches")]
if high_matches:
parts.append(f"{len(high_matches)} address(es) match known scam patterns")
return ". ".join(parts) + "." if parts else ""
def enrich_tool_response(
tool_name: str,
raw_result: dict[str, Any],
request_params: dict | None = None,
opt_out: bool = False,
) -> dict[str, Any]:
"""
Main entry point: enrich an x402 tool response with wallet intelligence.
Called after tool execution, before the response is sent to the client.
Returns the enriched result dict (mutates in place, also returns for chaining).
"""
if opt_out:
return raw_result
try:
r = _redis()
except Exception:
r = None
# Determine enrichment type
category = _is_enrichable(tool_name)
if not category:
return raw_result # Not an enrichable tool
# 1. Extract addresses
addresses = extract_addresses(raw_result, tool_name)
if not addresses:
return raw_result
# 2. Batch ClickHouse label lookup
labels, ch_ms = lookup_labels(addresses)
# 3. Identify sanctioned wallets
sanctioned = [a for a, lbls in labels.items() if any(line.is_sanctioned for line in lbls)]
# 4. RAG similarity search (only for wallet-centric tools)
similar_patterns = []
rag_ms = 0
if category in ("wallet", "security"):
similar_patterns, rag_ms = search_similar_patterns(addresses[:10], r)
# 5. Build enrichment result
enrichment = EnrichmentResult(
labels=labels,
sanctioned=sanctioned,
risk_summary=build_risk_summary(labels, sanctioned, similar_patterns),
similar_patterns=similar_patterns,
lookup_ms=ch_ms,
rag_ms=rag_ms,
)
# 5b. Scam pattern detection (wired from rag_service)
scam_flags = []
try:
import asyncio
from app.rag_service import detect_scam_patterns
for addr in addresses[:5]:
try:
result = asyncio.run(detect_scam_patterns({"address": addr, "chain": "ethereum"}, 0.65))
if result and result.get("risk_score", 0) > 0:
scam_flags.append(
{
"address": addr[:10] + "...",
"risk_score": result.get("risk_score", 0),
"patterns": result.get("patterns", [])[:3],
}
)
except Exception:
pass
except Exception:
pass
# 5c. LLM-synthesized summary (DeepSeek Flash, $0.0003/call)
llm_summary = ""
if enrichment.risk_summary and scam_flags:
llm_summary = _synthesize_enrichment(enrichment.risk_summary, scam_flags, tool_name)
# 5d. Cron intel injection: recent RMI intelligence briefings
intel_context = _load_recent_intel(r)
if intel_context:
raw_result["_intel"] = intel_context
# 5e. Confidence scoring: quantify data quality for every response
try:
from app.routers.x402_advanced_tools import compute_confidence
raw_result["_confidence"] = compute_confidence(raw_result)
except Exception:
pass
# 6. Merge into response
raw_result["_enrichment"] = {
"source": "wallet_memory_bank",
"labels_found": sum(1 for v in labels.values() if v),
"addresses_checked": len(addresses),
"sanctioned": sanction_count if (sanction_count := len(sanctioned)) > 0 else None,
"risk_summary": enrichment.risk_summary or None,
"llm_summary": llm_summary or None,
"scam_flags": scam_flags if scam_flags else None,
"label_details": {
a: [
{
"category": line.label_category,
"name": line.label_name,
"source": line.source,
"sanctioned": line.is_sanctioned,
}
for line in lbls
]
for a, lbls in labels.items()
if lbls
}
if any(v for v in labels.values())
else None,
"similar_patterns": similar_patterns if similar_patterns else None,
"performance_ms": round(enrichment.lookup_ms + enrichment.rag_ms, 1),
}
# Clean up None values
enrich = raw_result["_enrichment"]
for k in list(enrich.keys()):
if (
enrich[k] is None
or (isinstance(enrich[k], dict) and not enrich[k])
or (isinstance(enrich[k], list) and not enrich[k])
):
del enrich[k]
if not enrich.get("label_details") and not enrich.get("risk_summary"):
enrich["note"] = "No wallet labels found for queried addresses"
# 7. Alert hooks for high-risk findings
if sanction_count > 0:
_fire_enrichment_alert(tool_name, "sanctioned", sanctioned, r)
if scam_flags:
_fire_enrichment_alert(tool_name, "scam_pattern", [s["address"] for s in scam_flags], r)
# 8. Track addresses for RAG discovery pipeline
if addresses:
try:
for addr in addresses[:5]:
r.lpush(
"x402:recent:addresses",
json.dumps(
{
"address": addr,
"tool": tool_name,
"timestamp": datetime.utcnow().isoformat(),
}
),
)
r.ltrim("x402:recent:addresses", 0, 999)
except Exception:
pass
return raw_result
# ── Cron Intel Injection ─────────────────────────────────────
def _load_recent_intel(r=None) -> dict | None:
"""Load the latest RMI intel briefing from Redis for injection into tool results.
Cron jobs (Daily Briefing, Intel Digest) write summaries to x402:intel:latest.
Returns None if no recent intel available (older than 12h).
"""
try:
if not r:
r = _redis()
data = r.get("x402:intel:latest")
if not data:
return None
intel = json.loads(data)
age_h = (time.time() - intel.get("timestamp", 0)) / 3600
if age_h > 12:
return None # Stale
return {
"source": "rmi_cron_intel",
"briefing": intel.get("summary", "")[:500],
"age_hours": round(age_h, 1),
"scanner_alerts": intel.get("alerts", 0),
"tokens_scanned": intel.get("tokens_scanned", 0),
"timestamp": intel.get("timestamp", 0),
}
except Exception:
return None
# ── LLM Synthesis ────────────────────────────────────────────
def _synthesize_enrichment(risk_summary: str, scam_flags: list, tool_name: str) -> str:
"""Use DeepSeek Flash to synthesize a human-readable enrichment paragraph."""
try:
import json
import os
import urllib.request
key_b64 = os.getenv("LLM_API_KEY_B64", "")
if not key_b64:
return ""
import base64
api_key = base64.b64decode(key_b64).decode()
prompt = (
f"A crypto security tool '{tool_name}' analyzed some addresses and found: "
f"{risk_summary} "
f"Scam patterns detected: {json.dumps(scam_flags)[:500]}. "
f"Write ONE sentence (max 80 chars) summarizing the key risk finding concisely."
)
body = json.dumps(
{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 60,
"temperature": 0.3,
}
).encode()
req = urllib.request.Request(
"https://api.deepseek.com/v1/chat/completions",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
)
r = urllib.request.urlopen(req, timeout=5)
d = json.loads(r.read())
return d["choices"][0]["message"]["content"].strip().strip('"')
except Exception:
return "" # Best-effort - never block enrichment on LLM failure
# ── Alert Hooks ──────────────────────────────────────────────
def _fire_enrichment_alert(tool_name: str, alert_type: str, data: list, r=None):
"""Fire alert when enrichment finds high-risk addresses."""
try:
if not r:
r = _redis()
alert = json.dumps(
{
"tool": tool_name,
"type": alert_type,
"data": data,
"timestamp": __import__("datetime").datetime.utcnow().isoformat(),
}
)
r.lpush("x402:alerts:high_risk", alert)
r.ltrim("x402:alerts:high_risk", 0, 999) # Keep last 1000
logger.warning(f"ENRICHMENT ALERT: {alert_type} in {tool_name}: {data}")
except Exception:
pass
# ── Cross-Chain Label Propagation ────────────────────────────
def propagate_labels_cross_chain():
"""
One-shot cross-chain propagation: copy phishing/scam/sanctioned labels
to all chains where the same address exists. Runs on startup + cron.
"""
try:
ch = _ch()
# Find addresses labeled as scam/phishing/sanctioned on one chain
# and copy those labels to all other chains where the address exists
count = ch.execute("""
INSERT INTO wallet_memory.wallet_labels
(address, chain_id, label_name, label_category, label_subtype, source, is_sanctioned, loaded_at)
SELECT DISTINCT
a.address,
a.chain_id AS chain_id,
scam.label_name,
scam.label_category,
scam.label_subtype || ' (cross-chain from ' || scam.chain_id || ')',
'cross_chain_propagation',
scam.is_sanctioned,
now()
FROM wallet_memory.wallet_labels AS scam
JOIN wallet_memory.wallet_labels AS a
ON a.address = scam.address AND a.chain_id != scam.chain_id
WHERE scam.label_category IN ('phish-hack','scam','sanctioned','etherscan-phish-hack-list')
AND a.chain_id NOT IN ('','unknown')
AND scam.chain_id NOT IN ('','unknown')
LIMIT 100000
""")
logger.info(f"Cross-chain propagation: {count} labels propagated")
return count
except Exception as e:
logger.warning(f"Cross-chain propagation failed: {e}")
return 0
# ── Lightweight: cache-only enrichment for repeated queries ──
def get_cached_enrichment(address: str) -> dict | None:
"""Check if an address has cached enrichment data."""
try:
r = _redis()
cached = r.get(f"x402:enrich:{address}")
if cached:
return json.loads(cached)
except Exception:
pass
return None
# ── Etherscan V2 Live Contract Name Lookups ──
def _get_etherscan_key() -> str:
"""Load Etherscan API key from .env. Returns empty string if not configured."""
try:
import os
from dotenv import load_dotenv
load_dotenv("/app/.env", override=True)
return os.getenv("ETHERSCAN_API_KEY", "")
except Exception:
return ""
def lookup_contract_names(addresses: list[str], chain: str = "ethereum") -> dict[str, str]:
"""
Live Etherscan V2 contract name lookups for unknown addresses.
Returns {address: contract_name} dict.
Rate-limited to 5/sec (free tier). Skips addresses already in CH labels.
"""
key = _get_etherscan_key()
if not key:
return {}
# Map chain names to Etherscan chain IDs
CHAIN_MAP = {
"ethereum": 1,
"eth": 1,
"bsc": 56,
"polygon": 137,
"matic": 137,
"arbitrum": 42161,
"arb": 42161,
"optimism": 10,
"op": 10,
"base": 8453,
"avalanche": 43114,
"avax": 43114,
"fantom": 250,
"ftm": 250,
"gnosis": 100,
}
cid = CHAIN_MAP.get(chain.lower(), 1)
results = {}
import json
import time
import urllib.parse
import urllib.request
for addr in addresses[:20]: # Max 20 per call (free tier 5/sec + timeout budget)
try:
params = {
"chainid": cid,
"module": "contract",
"action": "getsourcecode",
"address": addr,
"apikey": key,
}
url = f"https://api.etherscan.io/v2/api?{urllib.parse.urlencode(params)}"
r = urllib.request.urlopen(url, timeout=5)
d = json.loads(r.read())
if d.get("status") == "1" and d.get("result"):
contract = d["result"][0]
name = contract.get("ContractName", "")
if name and name != addr:
results[addr] = name
except Exception:
pass
time.sleep(0.25) # Respect 5/sec rate limit
return results
def preload_enrichment_cache(addresses: list[str]):
"""
Background preload: fetch and cache enrichment for hot addresses.
Intended for high-traffic addresses (top wallets, trending tokens).
"""
for addr in addresses[:50]:
try:
r = _redis()
if r.exists(f"x402:enrich:{addr}"):
continue
result, _ = lookup_labels([addr])
labels = result.get(addr, [])
if labels:
r.setex(
f"x402:enrich:{addr}",
1800,
json.dumps({"labels": [{"category": line.label_category, "name": line.label_name} for line in labels]}),
)
except Exception:
continue