"""RMI Backend - router mounting. Single source of truth for every router. Adding a new domain: 1. Create the module with a `router = APIRouter(...)` attribute 2. Add the import path to ROUTER_MODULES below 3. Done - factory.create_app() picks it up automatically Per v4.0 §T01 + ADR-0001, this replaces the hardcoded lists that existed in the old main.py. Each mount is isolated - one failure doesn't break the others. """ from __future__ import annotations import importlib import logging from typing import Any, Final log = logging.getLogger(__name__) # Canonical router list (single source of truth) # Order matters for path resolution. v1 routers are mounted at /api/v1/*. # Domain routers (news, reports, x402) self-prefix. ROUTER_MODULES: Final[list[str]] = [ # Core system routes "app.core.health_route", # /health, /live, /ready "app.core.metrics", # /metrics "app.homepage", # /, /version # v1 API (thin HTTP layer) "app.api.v1.auth.alerts", # /api/v1/alerts/* "app.api.v1.public.wallet", # /api/v1/wallet/* "app.api.v1.public.token", # /api/v1/token/* "app.api.v1.public.scanner", # /api/v1/scanner/* "app.api.v1.rag.search", # /api/v1/rag/v2/* "app.api.v1.admin.alerts_webhook", # /api/v1/admin/alerts/webhook "app.api.v1.admin.glitchtip_test", # /api/v1/_test/* (T07) "app.api.v1.catalog", # /api/v1/catalog/* "app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON) # Domain facades (per v4.0 T28-T34) "app.domains.news", # /api/v1/news/* "app.domains.news.admin_router", # /api/v1/news/_admin/* "app.domains.reports", # /api/v1/reports/* "app.domains.x402", # /api/v1/x402/* # x402 MCP + Discovery + Docs "app.routers.x402_mcp_handler", # /mcp/x402, /.well-known/x402 "app.routers.x402_docs", # /x402/docs, /x402/sandbox/* "app.routers.x402_growth", # /api/v1/x402/webhooks/*, /usage, /pay, /affiliates "app.routers.x402_docs_v2", # /docs/v2/quickstart, /docs/v2/api-reference "app.routers.stripe_integration", # /api/v1/stripe/*, /api/v1/credits/* # Wire-in Wave 1 - AUDIT-2026-Q3 Phase 2 # Mounted 2026-07-07. Re-validated "dead" routers, all import cleanly, # all prefixes non-conflicting with existing mounts. # # Routers (24) - total ~140 routes added: "app.routers.bulletin", # /api/v1/bulletin/* (21 routes, supabase) "app.routers.bulletin_board", # /api/v1/bulletin-board/* (22 routes, app/bulletin_board.py) "app.routers.intelligence_router", # /api/v1/intelligence/* (19 routes, n8n-fed) "app.routers.intelligence_panel", # /api/v1/intelligence/* (4 routes, /feed /latest /crypto-fear-index) "app.routers.chat", # /api/v1/chat/* (5 routes, AI chat surface) "app.routers.address_profiler", # /api/v1/address-profiler/* (1 route, scanner ext) "app.routers.chain_comparability", # /api/v1/chain-compare/* (2 routes, multi-chain) "app.routers.honeypot_map", # /api/v1/honeypot-map/* (3 routes, unique viz) "app.routers.label_lookup", # /api/v1/labels/* (1 route, CF worker enrichment) "app.routers.whale_alerts", # /api/v1/whale-alerts/* (5 routes, whale tracker) "app.routers.lp_health", # /api/v1/lp-health/* (5 routes, LP monitor) "app.routers.ai_stream", # /api/v1/ai/* (3 routes, SSE infra) "app.routers.rugcharts", # /api/v1/rugcharts/* (3 routes, RugCharts backend) "app.routers.bubble_maps_router", # /api/v1/rugmaps/* (7 routes, RugMaps viz) "app.routers.mev_advisory", # /api/v1/mev-advisory/* (2 routes, MEV warning) "app.routers.impersonation_detector", # /api/v1/impersonation/* (1 route, forensics) "app.routers.rug_recovery", # /api/v1/rug-recovery/* (2 routes, recovery indexer) "app.routers.laundry_matcher", # /api/v1/laundry-matcher/* (2 routes, copycat detect) "app.routers.death_clock", # /api/v1/death-clock/* (1 route, Token Death Clock) "app.routers.token_cv", # /api/v1/token-cv/* (2 routes, On-Chain CV) "app.routers.tvl_verifier", # /api/v1/tvl-verify/* (2 routes, DeFiLlama mismatch) "app.routers.contract_analyzer", # /api/v1/contract-analyzer/* (2 routes, SENTINEL ext) "app.routers.criminal_clusters", # /api/v1/criminal-clusters/* (3 routes, Real-CATS) "app.rag_metrics_api", # /api/v1/rag/metrics (1 route, uses LIVE rag_observability) # # Wire-in Wave 2 — AUDIT-2026-Q3.md Phase 2.2 — mounted 2026-07-07. # All routers below import cleanly with LIVE deps; no dead imports. # Mounted 2026-07-07 — 6 medium-wiring routers + news retry: "app.routers.news", # /api/v1/news/{feed,headlines,sources,sentiment,categories,stats,comment,comments,internal/scanner-alert} (9 routes, news_service.py live). Wave 1 deferred; resolved by moving domain.news catch-all to /api/v1/articles/{news_id}. "app.routers.market_intel_router", # /api/v1/market/* (28 routes, CoinGecko + DexScreener + Polymarket). Uses LIVE app.services.prediction_market_intel. "app.routers.prediction_market_router", # /api/v1/prediction-markets/* (6 routes, prediction_market_service.py live) "app.routers.scam_school", # /scam-school/* (11 routes, education platform). No prefix — paths absolute. "app.routers.wallet_clustering_router", # /api/v1/wallet-clusters/* (8 routes, wallet_clustering.py live; depends on app.{ai_router,bundle_detector,cluster_detection,entity_registry,fraud_gnn,unified_provider,wallet_clustering,spam_registry,chain_cache}) "app.routers.security_intel", # /api/v1/security/* (31 routes, crypto security stack). Depends on LIVE app.{cross_chain_correlator,ml_anomaly,onchain_analyzer}. "app.routers.mev_sniper", # /api/v1/mev-sniper/* (2 routes: /signals + /chains). No internal app.* deps. MEV Sniper = premium scanner feature. # # Wire-in Wave 3 — AUDIT-2026-Q3.md Phase 2.3 — mounted 2026-07-07. "app.routers.unified_scanner_router", # /api/v2/scanner/{token,wallet}/scan (2 routes, v2 scanner). # # DEFERRED Wave 3 (2): # "app.routers.unified_wallet_scanner" — NO `router` APIRouter attribute. # Module exposes `get_wallet_scanner()` consumed by unified_scanner_router. # Wire-in deferred: not a router, would shadow /api/v2/scanner/wallet/scan # if exposed as a thin wrapper. Already reachable from app.unified_wallet_scanner # which is imported by unified_scanner_router. # "app.routers.admin_extensions" — DORMANT per audit (darkroom-specific, 25 routes # at /api/v1/admin/*). Would shadow /api/v1/admin/alerts_webhook (mounted Wave 1). # Kept in tree per audit, not wired. ] def mount_all(app: Any) -> int: """Mount every router. Returns count of successful mounts. Each mount is isolated - one failure does not break the rest. """ mounted = 0 for module_path in ROUTER_MODULES: if _try_mount(app, module_path): mounted += 1 return mounted def _try_mount(app: Any, module_path: str) -> bool: """Try to import + mount a single router. Returns True on success.""" try: module = importlib.import_module(module_path) router = getattr(module, "router", None) if router is None: log.warning("router_missing path=%s (no `router` attribute)", module_path) return False app.include_router(router) log.info("router_mounted path=%s", module_path) return True except Exception as exc: log.warning( "router_mount_failed path=%s err=%s: %s", module_path, type(exc).__name__, exc, ) return False def register_external_router(module_path: str) -> None: """Allow third-party code to register additional routers at runtime. Plugins / MCP servers / worker modules can call this from their own __init__.py to add routes to the main app. """ if module_path not in ROUTER_MODULES: ROUTER_MODULES.append(module_path) log.info("router_registered_external path=%s", module_path)