""" RMI Facilitator Health Monitor =============================== Periodically pings all x402 payment facilitators and tracks: - Latency (ms) - Success rate (% of last 100 pings) - Last seen timestamp - Status (healthy/degraded/dead) Auto-failover: enforcement middleware reads health scores and routes around degraded/dead facilitators. Author: RMI Development Date: 2026-06-05 """ import logging from datetime import UTC, datetime from fastapi import APIRouter from fastapi.responses import JSONResponse from app.core.redis import get_redis logger = logging.getLogger("facilitator_health") router = APIRouter(prefix="/api/v1/x402/facilitator-health", tags=["facilitator-health"]) # ── Facilitator Definitions ─────────────────────────────────────── FACILITATORS = { "payai": { "name": "PayAI", "health_url": "https://facilitator.payai.network/health", "chains": [ "base", "solana", "ethereum", "arbitrum", "optimism", "polygon", "avalanche", "bsc", "fantom", "gnosis", ], "priority": 1, "timeout_ms": 5000, }, "coinbase_cdp": { "name": "Coinbase CDP", "health_url": None, # No public health endpoint — check via payment verification "chains": ["base", "ethereum"], "priority": 2, "timeout_ms": 10000, }, "cloudflare_x402": { "name": "Cloudflare x402", "health_url": None, # Check via worker health "chains": ["base", "ethereum", "arbitrum", "optimism", "polygon"], "priority": 3, "timeout_ms": 5000, }, "primev": { "name": "Primev", "health_url": None, "chains": ["ethereum"], "priority": 4, "timeout_ms": 10000, }, "eip7702": { "name": "EIP-7702", "health_url": None, "chains": [ "ethereum", "base", "arbitrum", "optimism", "polygon", "avalanche", "bsc", "fantom", "gnosis", ], "priority": 5, "timeout_ms": 10000, }, "asterpay": { "name": "AsterPay (SEPA/EUR)", "health_url": None, "chains": ["sepa"], "priority": 6, "timeout_ms": 10000, }, "x402_rs": { "name": "x402-rs (self-hosted)", "health_url": None, "chains": ["base", "ethereum"], "priority": 7, "timeout_ms": 5000, }, # OFFLINE facilitators — tracked but marked dead "pieverse": { "name": "Pieverse", "health_url": None, "chains": [], "priority": 99, "timeout_ms": 5000, "status_override": "dead", }, "merx_tron": { "name": "MERX TRON", "health_url": None, "chains": [], "priority": 99, "timeout_ms": 5000, "status_override": "dead", }, "satoshi": { "name": "Satoshi", "health_url": None, "chains": [], "priority": 99, "timeout_ms": 5000, "status_override": "dead", }, } # ── Redis Helper ───────────────────────────────────────────────── async def get_all_health(): """Get health status for all facilitators.""" result = {} for name in FACILITATORS: result[name] = get_facilitator_health(name) # Summary healthy = sum(1 for v in result.values() if v.get("status") == "healthy") degraded = sum(1 for v in result.values() if v.get("status") == "degraded") dead = sum(1 for v in result.values() if v.get("status") in ("dead",)) return JSONResponse( content={ "facilitators": result, "summary": { "total": len(FACILITATORS), "healthy": healthy, "degraded": degraded, "dead": dead, "offline": sum(1 for v in FACILITATORS.values() if v.get("status_override") == "dead"), }, "timestamp": datetime.now(UTC).isoformat(), } ) @router.get("/status/{name}") async def get_facilitator_status(name: str): """Get health status for a specific facilitator.""" if name not in FACILITATORS: return JSONResponse( status_code=404, content={ "error": f"Facilitator '{name}' not found", "available": list(FACILITATORS.keys()), }, ) return JSONResponse(content=get_facilitator_health(name)) @router.get("/chains/{chain}") async def get_facilitators_for_chain(chain: str): """Get healthy facilitators for a specific chain.""" facilitators = get_healthy_facilitators_for_chain(chain) return JSONResponse( content={ "chain": chain, "facilitators": facilitators, "count": len(facilitators), } ) @router.post("/check/{name}") async def trigger_health_check(name: str): """Manually trigger a health check for a facilitator.""" if name not in FACILITATORS: return JSONResponse( status_code=404, content={"error": f"Facilitator '{name}' not found"}, ) config = FACILITATORS[name] result = await ping_facilitator(name, config) record_health(name, result) return JSONResponse( content={ "facilitator": name, "result": result, } ) # ── Payment Success Tracking ───────────────────────────────────── def record_payment_success(facilitator_name: str, success: bool): """Record whether a payment via this facilitator succeeded or failed. Called by the payment verification code after each payment attempt. """ r = get_redis() if not r: return total_key = f"rmi:facilitator:{facilitator_name}:payment_total" success_key = f"rmi:facilitator:{facilitator_name}:payment_success" r.incr(total_key) if success: r.incr(success_key) # Decay old counters (reset daily) today = datetime.now(UTC).strftime("%Y-%m-%d") r.incr(f"rmi:facilitator:{facilitator_name}:payment_total:{today}") if success: r.incr(f"rmi:facilitator:{facilitator_name}:payment_success:{today}")