""" Institutional-grade x402 tools — portfolio risk, DeFi analysis, MEV detection. Cross-Chain Portfolio Risk — unified risk across chains DeFi Position Analyzer — LP, impermanent loss, yield sustainability MEV/Sandwich Detection — sandwich attacks, frontrunning, arbitrage extraction """ import hashlib 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.institutional") router = APIRouter() # ═══════════════════════════════════════════════════════════ # Models # ═══════════════════════════════════════════════════════════ class PortfolioRequest(BaseModel): addresses: list[str] = Field(..., min_items=1, max_items=20, description="Wallet addresses across any chains") chains: list[str] | None = Field(default=None, description="Chain per address (auto-detect if omitted)") class DeFiRequest(BaseModel): address: str = Field(..., description="Wallet or LP position address") chain: str = Field(default="base") class MEVRequest(BaseModel): address: str = Field(..., description="Wallet or token address to check for MEV exposure") chain: str = Field(default="ethereum") tx_hash: str | None = Field(default=None, description="Specific transaction to analyze") # ═══════════════════════════════════════════════════════════ # Redis helpers # ═══════════════════════════════════════════════════════════ def _redis(): import redis as _r return _r.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 _cache_get(tool: str, params: dict) -> dict | None: try: key = f"x402:cache:{tool}:{hashlib.sha256(json.dumps(params, sort_keys=True).encode()).hexdigest()[:16]}" data = _redis().get(key) return json.loads(data) if data else None except Exception: return None def _cache_set(tool: str, params: dict, result: dict, ttl: int = 60): try: key = f"x402:cache:{tool}:{hashlib.sha256(json.dumps(params, sort_keys=True).encode()).hexdigest()[:16]}" _redis().setex(key, ttl, json.dumps(result)) except Exception: pass # ═══════════════════════════════════════════════════════════ # TOOL: Cross-Chain Portfolio Risk ($0.20) # ═══════════════════════════════════════════════════════════ CHAIN_RPC_MAP = { "ethereum": "https://eth.llamarpc.com", "base": "https://mainnet.base.org", "bsc": "https://bsc-dataseed.binance.org", "polygon": "https://polygon-rpc.com", "arbitrum": "https://arb1.arbitrum.io/rpc", "optimism": "https://mainnet.optimism.io", "avalanche": "https://api.avax.network/ext/bc/C/rpc", "fantom": "https://rpc.ftm.tools", "gnosis": "https://rpc.gnosischain.com", "solana": "https://api.mainnet-beta.solana.com", } CHAIN_NATIVE_SYMBOL = { "ethereum": "ETH", "base": "ETH", "bsc": "BNB", "polygon": "POL", "arbitrum": "ETH", "optimism": "ETH", "avalanche": "AVAX", "fantom": "FTM", "gnosis": "xDAI", "solana": "SOL", } async def _get_balance_evm(chain: str, address: str) -> float | None: rpc = CHAIN_RPC_MAP.get(chain) if not rpc: return None try: async with ( aiohttp.ClientSession() as s, s.post( rpc, json={ "jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params": [address, "latest"], }, timeout=aiohttp.ClientTimeout(total=8), ) as r, ): if r.status == 200: data = await r.json() return int(data.get("result", "0x0"), 16) / 1e18 except Exception: pass return None async def _get_balance_solana(address: str) -> float | None: try: async with aiohttp.ClientSession() as s, s.post( "https://api.mainnet-beta.solana.com", json={"jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [address]}, timeout=aiohttp.ClientTimeout(total=8), ) as r: if r.status == 200: data = await r.json() return data.get("result", {}).get("value", 0) / 1e9 except Exception: pass return None @router.post("/portfolio_risk") async def portfolio_risk(req: PortfolioRequest): """Unified risk dashboard for wallets across multiple chains. Aggregates: balances, risk scores, label associations, MEV exposure, and scam indicators into a single portfolio-wide risk report. """ try: addresses = req.addresses[:20] chains = (req.chains or ["auto"] * len(addresses))[: len(addresses)] cached = _cache_get("portfolio_risk", {"addresses": addresses, "chains": chains}) if cached: return cached wallets = [] total_balance_usd = 0 risk_flags = [] sources = [] for i, addr in enumerate(addresses): chain = chains[i] if i < len(chains) else "auto" wallet = {"address": addr, "assigned_chain": chain} # Auto-detect chain by address format if chain == "auto": if addr.startswith("0x") and len(addr) == 42: chain = "ethereum" elif len(addr) in (43, 44) and not addr.startswith("0x"): chain = "solana" elif addr.startswith("T") and len(addr) == 34: chain = "tron" elif addr.startswith("1") or addr.startswith("3") or addr.startswith("bc1"): chain = "bitcoin" else: chain = "ethereum" wallet["detected_chain"] = chain # Get balance if chain == "solana": bal = await _get_balance_solana(addr) if bal is not None: wallet["balance"] = round(bal, 6) wallet["asset"] = "SOL" # Approximate USD (SOL ~$140) usd = bal * 140 wallet["balance_usd_approx"] = round(usd, 2) total_balance_usd += usd sources.append("solana_rpc") elif chain in CHAIN_RPC_MAP: bal = await _get_balance_evm(chain, addr) if bal is not None: wallet["balance"] = round(bal, 6) wallet["asset"] = CHAIN_NATIVE_SYMBOL.get(chain, "ETH") # Approximate USD based on chain eth_price = ( 3200 if chain in ("ethereum", "base", "arbitrum", "optimism") else 600 if chain == "bsc" else 0.4 if chain == "polygon" else 25 if chain == "avalanche" else 0.5 if chain == "fantom" else 1 ) usd = bal * eth_price wallet["balance_usd_approx"] = round(usd, 2) total_balance_usd += usd sources.append(f"{chain}_rpc") # Risk check via reputation score try: async with aiohttp.ClientSession() as s, s.post( "http://localhost:8000/api/v1/x402-tools/reputation_score", json={"address": addr, "chain": chain}, timeout=aiohttp.ClientTimeout(total=15), ) as r: if r.status == 200: rep = await r.json() wallet["risk_score"] = rep.get("trust_score") wallet["risk_tier"] = rep.get("tier") if rep.get("flags"): risk_flags.extend([{**f, "address": addr[:12] + "..."} for f in rep["flags"]]) sources.append("reputation_score") except Exception: pass # Label check try: from app.routers.x402_premium_tools import _lookup_labels_async labels = await _lookup_labels_async(addr) if labels: wallet["labels"] = [ {"name": line.get("label_name"), "category": line.get("label_category")} for line in labels[:3] ] sources.append("wallet_labels") except Exception: pass wallets.append(wallet) # Portfolio-wide scoring risk_scores = [w.get("risk_score", 50) for w in wallets if w.get("risk_score") is not None] avg_risk = sum(risk_scores) / len(risk_scores) if risk_scores else 50 if avg_risk >= 80: portfolio_tier = "LOW_RISK" elif avg_risk >= 60: portfolio_tier = "MODERATE" elif avg_risk >= 40: portfolio_tier = "ELEVATED" else: portfolio_tier = "HIGH_RISK" result = { "tool": "Cross-Chain Portfolio Risk", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "wallets_analyzed": len(wallets), "chains_covered": len({w.get("detected_chain", w.get("assigned_chain", "?")) for w in wallets}), "total_balance_usd_approx": round(total_balance_usd, 2), "portfolio_risk_score": round(avg_risk, 1), "portfolio_risk_tier": portfolio_tier, "wallets": wallets, "risk_flags": risk_flags[:10], "flag_count": len(risk_flags), "sources_used": list(set(sources)), "recommendation": ( "Portfolio appears healthy — continue monitoring" if portfolio_tier == "LOW_RISK" else "Some risk factors detected — review flagged wallets" if portfolio_tier == "MODERATE" else "Elevated risk — several wallets have concerning signals" if portfolio_tier == "ELEVATED" else "HIGH RISK — multiple critical flags across portfolio. Immediate review recommended." ), "guarantee": "Data delivered or auto-refund via x402 receipt", } _cache_set("portfolio_risk", {"addresses": addresses, "chains": chains}, result, ttl=90) return result except Exception as e: logger.error(f"Portfolio risk failed: {e}") raise HTTPException(status_code=500, detail=str(e)) # ═══════════════════════════════════════════════════════════ # TOOL: DeFi Position Analyzer ($0.15) # ═══════════════════════════════════════════════════════════ @router.post("/defi_position") async def defi_position(req: DeFiRequest): """Analyze DeFi positions — LP holdings, impermanent loss, yield sustainability. Checks: liquidity provision, staking positions, lending positions, yield farming exposure, impermanent loss estimation, and protocol risk. """ try: addr = req.address.strip() chain = req.chain or "base" t0 = time.time() cached = _cache_get("defi_position", {"address": addr, "chain": chain}) if cached: return cached positions = [] sources = [] total_tvl = 0 warnings = [] # ── DeFiLlama position lookup ── # Note: DeFiLlama doesn't have a direct "positions by wallet" endpoint # We check known protocols for LP/staking positions # ── DexScreener LP check ── try: async with aiohttp.ClientSession() as s: url = f"https://api.dexscreener.com/latest/dex/search?q={addr[:12]}" async with s.get(url, timeout=aiohttp.ClientTimeout(total=8)) as r: if r.status == 200: data = await r.json() pairs = data.get("pairs", []) if pairs: sources.append("dexscreener") for p in pairs[:5]: liq = p.get("liquidity", {}).get("usd", 0) or 0 if liq > 0: pair_addr = p.get("pairAddress", "") base = p.get("baseToken", {}) quote = p.get("quoteToken", {}) pos = { "type": "liquidity_provision", "pair": f"{base.get('symbol', '?')}/{quote.get('symbol', '?')}", "pair_address": pair_addr, "liquidity_usd": liq, "dex": p.get("dexId", "unknown"), "chain": p.get("chainId", chain), "volume_24h": p.get("volume", {}).get("h24", 0), "price_change_24h": p.get("priceChange", {}).get("h24", 0), } total_tvl += liq # Impermanent loss estimation pc = abs(p.get("priceChange", {}).get("h24", 0) or 0) if pc > 0: # IL formula: 2*sqrt(price_ratio)/(1+price_ratio) - 1 price_ratio = 1 + pc / 100 il = abs(2 * (price_ratio**0.5) / (1 + price_ratio) - 1) * 100 pos["impermanent_loss_est"] = round(il, 2) if il > 5: warnings.append(f"High IL risk ({il:.1f}%) on {pos['pair']}") elif il > 2: pos["il_warning"] = f"Moderate IL: {il:.1f}%" # Volume/Liquidity health vol = pos["volume_24h"] if liq > 0 and vol > 0: turnover = vol / liq pos["daily_turnover"] = round(turnover, 2) if turnover > 1: pos["yield_potential"] = "high" elif turnover > 0.3: pos["yield_potential"] = "moderate" else: pos["yield_potential"] = "low" positions.append(pos) except Exception: pass # ── DeFiLlama protocol TVL context ── try: async with aiohttp.ClientSession() as s: async with s.get("https://api.llama.fi/protocols", timeout=aiohttp.ClientTimeout(total=8)) as r: if r.status == 200: data = await r.json() sources.append("defillama") # Find protocols the wallet might be using dexes_in_positions = {p.get("dex", "") for p in positions} protocol_risks = [] for proto in data[:100]: if proto.get("name", "").lower() in [d.lower() for d in dexes_in_positions]: protocol_risks.append( { "name": proto.get("name"), "tvl": proto.get("tvl", 0), "category": proto.get("category", ""), "audits": proto.get("audits", 0), "audit_links": proto.get("audit_links", [])[:3], } ) if protocol_risks: for pr in protocol_risks: if pr.get("audits", 0) == 0: warnings.append(f"No audits found for {pr['name']} — protocol risk elevated") except Exception: pass # ── Compute overall assessment ── risk_level = "low" if len(warnings) >= 3 or any("High IL risk" in w for w in warnings): risk_level = "high" elif len(warnings) >= 1: risk_level = "moderate" result = { "tool": "DeFi Position Analyzer", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "address": addr, "chain": chain, "positions_found": len(positions), "total_tvl_usd": round(total_tvl, 2), "positions": positions, "warnings": warnings if warnings else None, "overall_risk": risk_level, "recommendation": ( "Positions appear healthy — standard monitoring sufficient" if risk_level == "low" else "Some risk factors — review impermanent loss exposure and protocol audits" if risk_level == "moderate" else "HIGH RISK — significant IL exposure and/or unaudited protocols. Consider reducing position sizes." ), "sources_used": sources, "performance_ms": round((time.time() - t0) * 1000, 1), "guarantee": "Data delivered or auto-refund via x402 receipt", } _cache_set("defi_position", {"address": addr, "chain": chain}, result) return result except Exception as e: logger.error(f"DeFi position failed: {e}") raise HTTPException(status_code=500, detail=str(e)) # ═══════════════════════════════════════════════════════════ # TOOL: MEV / Sandwich Attack Detection ($0.15) # ═══════════════════════════════════════════════════════════ @router.post("/mev_detect") async def mev_detect(req: MEVRequest): """Detect MEV exploitation — sandwich attacks, frontrunning, backrunning. Analyzes transaction history for MEV patterns: - Sandwich attacks (buy before, sell after) - Frontrunning (same-block order manipulation) - Arbitrage extraction - MEV bot wallet identification """ try: addr = req.address.strip() chain = req.chain or "ethereum" tx_hash = req.tx_hash t0 = time.time() cached = _cache_get("mev_detect", {"address": addr, "chain": chain, "tx_hash": tx_hash}) if cached: return cached sources = [] mev_attacks = [] mev_exposure_usd = 0 risk_signals = [] # ── EigenPhi MEV detection ── if chain in ("ethereum", "base", "bsc", "polygon", "arbitrum"): try: async with aiohttp.ClientSession() as s: url = f"https://eigenphi.io/api/v1/mev/address/{addr}?chain={chain}" async with s.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r: if r.status == 200: data = await r.json() if data.get("mev_attacks"): sources.append("eigenphi") for attack in data["mev_attacks"][:10]: attack_type = attack.get("type", "unknown") mev_attacks.append( { "type": attack_type, "tx_hash": attack.get("tx_hash", "")[:16] + "...", "block": attack.get("block_number"), "profit_usd": attack.get("profit_usd", 0), "attacker": attack.get("attacker", "")[:12] + "...", "timestamp": attack.get("timestamp"), } ) mev_exposure_usd += attack.get("profit_usd", 0) except Exception: pass # ── Etherscan tx analysis for sandwich patterns ── if chain in ( "ethereum", "base", "bsc", "polygon", "arbitrum", "optimism", "avalanche", "fantom", "gnosis", ): try: api_key = os.getenv("ETHERSCAN_API_KEY", "") if api_key: chain_id_map = { "ethereum": 1, "base": 8453, "bsc": 56, "polygon": 137, "arbitrum": 42161, "optimism": 10, "avalanche": 43114, "fantom": 250, "gnosis": 100, } cid = chain_id_map.get(chain, 1) async with aiohttp.ClientSession() as s: if tx_hash: url = f"https://api.etherscan.io/v2/api?chainid={cid}&module=account&action=txlistinternal&txhash={tx_hash}&apikey={api_key}" else: url = f"https://api.etherscan.io/v2/api?chainid={cid}&module=account&action=txlist&address={addr}&page=1&offset=10&sort=desc&apikey={api_key}" async with s.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r: if r.status == 200: data = await r.json() txs = data.get("result", []) if txs and isinstance(txs, list): sources.append("etherscan") # Group by block to detect same-block patterns from collections import defaultdict block_txs = defaultdict(list) for tx in txs[:50]: block = tx.get("blockNumber") if block: block_txs[block].append(tx) for block, bt in block_txs.items(): if len(bt) >= 3: # Multiple txs in same block — possible sandwich buys = [ t for t in bt if t.get("from", "").lower() != addr.lower() and t.get("to", "").lower() == addr.lower() ] sells = [ t for t in bt if t.get("from", "").lower() == addr.lower() and t.get("to", "").lower() != addr.lower() ] if buys and sells: risk_signals.append( { "block": block, "pattern": "possible_sandwich", "buys_in_block": len(buys), "sells_in_block": len(sells), "detail": f"Block {block} has {len(buys)} incoming + {len(sells)} outgoing — possible sandwich attack", } ) except Exception: pass # ── MEV bot wallet check via labels ── try: from app.routers.x402_premium_tools import _lookup_labels_async labels = await _lookup_labels_async(addr) if labels: sources.append("wallet_labels") mev_labels = [ line for line in labels if any( kw in (line.get("label_name", "") + line.get("label_category", "")).lower() for kw in ("mev", "sandwich", "frontrun", "arbitrage", "bot") ) ] if mev_labels: risk_signals.append( { "pattern": "mev_bot_wallet", "detail": f"Address labeled as MEV-related: {mev_labels[0].get('label_name')}", "source": mev_labels[0].get("source"), } ) except Exception: pass # ── DexScreener for sudden price impact check ── try: async with aiohttp.ClientSession() as s: url = f"https://api.dexscreener.com/latest/dex/search?q={addr[:12]}" async with s.get(url, timeout=aiohttp.ClientTimeout(total=8)) as r: if r.status == 200: data = await r.json() pairs = data.get("pairs", []) if pairs: sources.append("dexscreener") for p in pairs[:3]: pc = p.get("priceChange", {}) if abs(pc.get("h24", 0) or 0) > 30: risk_signals.append( { "pattern": "extreme_volatility", "pair": f"{p.get('baseToken', {}).get('symbol', '?')}/{p.get('quoteToken', {}).get('symbol', '?')}", "price_change_24h": pc.get("h24"), "detail": f"Extreme {pc.get('h24')}% price movement — possible MEV extraction impact", } ) except Exception: pass # ── Final assessment ── total_signals = len(mev_attacks) + len(risk_signals) if total_signals >= 5 or mev_exposure_usd > 10000: risk_level = "critical" recommendation = "CRITICAL — significant MEV exploitation detected. Immediate review of trading strategy and RPC endpoint required." elif total_signals >= 2 or mev_exposure_usd > 1000: risk_level = "high" recommendation = "High MEV exposure — consider using Flashbots/MEV protection RPC" elif total_signals >= 1: risk_level = "moderate" recommendation = "Some MEV activity detected — monitor and consider MEV-protected transactions" else: risk_level = "low" recommendation = "No significant MEV exploitation detected" result = { "tool": "MEV / Sandwich Attack Detection", "version": "1.0", "timestamp": datetime.utcnow().isoformat(), "address": addr, "chain": chain, "mev_attacks_detected": len(mev_attacks), "mev_attacks": mev_attacks[:10], "mev_exposure_usd": round(mev_exposure_usd, 2), "risk_signals": risk_signals[:10], "total_signals": total_signals, "risk_level": risk_level, "recommendation": recommendation, "sources_used": sources, "protection_suggestions": [ "Use Flashbots RPC for Ethereum transactions", "Enable MEV protection in your wallet settings", "Use private mempool submission for large trades", "Consider DEX aggregators with built-in MEV protection (1inch, CowSwap)", ] if risk_level in ("high", "critical") else None, "performance_ms": round((time.time() - t0) * 1000, 1), "guarantee": "Data delivered or auto-refund via x402 receipt", } _cache_set("mev_detect", {"address": addr, "chain": chain, "tx_hash": tx_hash}, result) return result except Exception as e: logger.error(f"MEV detect failed: {e}") raise HTTPException(status_code=500, detail=str(e))