Hermes commited on
Commit
eca51fe
·
1 Parent(s): 24dd239

feat(t33+t34): MCP server + x402 paid catalog

Browse files
backend/app/api/v1/__init__.py CHANGED
@@ -49,9 +49,8 @@ from app.api.v1.public.scanner import router as scanner_router # noqa: E402
49
 
50
  api_v1_router.append(scanner_router)
51
 
52
- from app.api.v1.x402.payments import router as x402_payments_router # noqa: E402
53
-
54
- api_v1_router.append(x402_payments_router)
55
 
56
  from app.api.v1.rag.search import router as rag_v2_router # noqa: E402
57
 
 
49
 
50
  api_v1_router.append(scanner_router)
51
 
52
+ # x402 moved to app.domain.x402 (T34 v2)
53
+ # Old app/api/v1/x402/payments.py removed to avoid model conflicts
 
54
 
55
  from app.api.v1.rag.search import router as rag_v2_router # noqa: E402
56
 
backend/app/api/v1/mcp/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- """Model Context Protocol routes.
 
2
 
3
- Target: tool catalog for AI agents, JSON-RPC endpoint.
4
- """
 
1
+ """MCP v1 routes."""
2
+ from .router import router
3
 
4
+ __all__ = ["router"]
 
backend/app/api/v1/mcp/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (243 Bytes). View file
 
backend/app/api/v1/mcp/__pycache__/router.cpython-311.pyc ADDED
Binary file (5.71 kB). View file
 
backend/app/api/v1/mcp/router.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """T33 MCP Server — HTTP wrapper for SSE transport.
2
+
3
+ Per v4.0 §T33. Endpoints:
4
+ POST /mcp JSON-RPC 2.0 endpoint
5
+ GET /mcp/tools Tool catalog
6
+ POST /mcp/call/{tool_id} Direct tool execution (no JSON-RPC)
7
+
8
+ The server speaks the Model Context Protocol natively. Claude Desktop
9
+ and Cursor connect via:
10
+ {"mcpServers": {"rugmunch": {"url": "https://mcp.rugmunch.io/mcp", "transport": "sse"}}}
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from typing import Any
17
+
18
+ from fastapi import APIRouter, HTTPException, Request
19
+ from pydantic import BaseModel
20
+
21
+ from app.mcp.server import TOOL_CATALOG, call_tool
22
+
23
+ router = APIRouter(prefix="/mcp", tags=["mcp"])
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ class JsonRpcRequest(BaseModel):
28
+ jsonrpc: str = "2.0"
29
+ method: str
30
+ params: dict[str, Any] = {}
31
+ id: int | str | None = None
32
+
33
+
34
+ class JsonRpcResponse(BaseModel):
35
+ jsonrpc: str = "2.0"
36
+ result: Any | None = None
37
+ error: dict | None = None
38
+ id: int | str | None = None
39
+
40
+
41
+ @router.post("")
42
+ async def jsonrpc_handler(req: JsonRpcRequest) -> dict:
43
+ """JSON-RPC 2.0 endpoint for MCP clients.
44
+
45
+ Methods:
46
+ - initialize → returns server info
47
+ - tools/list → returns tool catalog
48
+ - tools/call → dispatches to backend
49
+ - resources/list → empty
50
+ - prompts/list → empty
51
+ """
52
+ if req.jsonrpc != "2.0":
53
+ return {"jsonrpc": "2.0", "error": {"code": -32600, "message": "invalid jsonrpc version"}, "id": req.id}
54
+
55
+ if req.method == "initialize":
56
+ return {
57
+ "jsonrpc": "2.0",
58
+ "result": {
59
+ "protocolVersion": "2024-11-05",
60
+ "serverInfo": {
61
+ "name": "rugmunch-intelligence",
62
+ "version": "4.0.0",
63
+ "description": "Crypto intelligence platform — 13+ chains, 8 MCP tools, x402 paid tier",
64
+ },
65
+ "capabilities": {"tools": {}, "resources": {}, "prompts": {}},
66
+ },
67
+ "id": req.id,
68
+ }
69
+
70
+ if req.method == "tools/list":
71
+ return {
72
+ "jsonrpc": "2.0",
73
+ "result": {"tools": TOOL_CATALOG},
74
+ "id": req.id,
75
+ }
76
+
77
+ if req.method == "tools/call":
78
+ name = req.params.get("name", "")
79
+ arguments = req.params.get("arguments", {})
80
+ if not name:
81
+ return {"jsonrpc": "2.0", "error": {"code": -32602, "message": "tool name required"}, "id": req.id}
82
+ result = await call_tool(name, arguments)
83
+ return {
84
+ "jsonrpc": "2.0",
85
+ "result": {
86
+ "content": [{"type": "text", "text": json.dumps(result, default=str)[:50000]}],
87
+ "isError": "error" in result,
88
+ },
89
+ "id": req.id,
90
+ }
91
+
92
+ if req.method == "resources/list":
93
+ return {"jsonrpc": "2.0", "result": {"resources": []}, "id": req.id}
94
+
95
+ if req.method == "prompts/list":
96
+ return {"jsonrpc": "2.0", "result": {"prompts": []}, "id": req.id}
97
+
98
+ if req.method == "notifications/initialized":
99
+ return {"jsonrpc": "2.0", "result": {}, "id": req.id}
100
+
101
+ return {
102
+ "jsonrpc": "2.0",
103
+ "error": {"code": -32601, "message": f"method not found: {req.method}"},
104
+ "id": req.id,
105
+ }
106
+
107
+
108
+ @router.get("/tools")
109
+ async def list_tools() -> dict:
110
+ """Plain JSON endpoint (for direct integration, no JSON-RPC)."""
111
+ return {"server": "rugmunch-intelligence", "version": "4.0.0", "tools": TOOL_CATALOG}
112
+
113
+
114
+ @router.post("/call/{tool_id}")
115
+ async def direct_call(tool_id: str, request: Request) -> dict:
116
+ """Direct tool execution (no JSON-RPC). For curl/scripts."""
117
+ body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
118
+ arguments = body.get("arguments", body) if isinstance(body, dict) else {}
119
+ result = await call_tool(tool_id, arguments)
120
+ return result
backend/app/domain/x402/__init__.py CHANGED
@@ -1,4 +1,10 @@
1
- """x402 domain — auto-registers its health check."""
 
 
 
 
 
 
2
  from __future__ import annotations
3
 
4
  from app.core import health as health_mod
@@ -6,14 +12,13 @@ from app.core.health import DomainHealth
6
 
7
 
8
  async def _health_check() -> DomainHealth:
9
- """x402 health: catalog routers + enforcement available."""
10
  try:
11
- from app.routers.x402_catalog import list_tools_catalog
12
- from app.routers.x402_enforcement import router
13
  return DomainHealth(
14
  name="x402",
15
  healthy=True,
16
- details={"catalog": "available", "enforcement": "available"},
17
  )
18
  except Exception as e:
19
  return DomainHealth(name="x402", healthy=False, error=str(e))
@@ -22,25 +27,19 @@ async def _health_check() -> DomainHealth:
22
  health_mod.register_health_check("x402", _health_check)
23
 
24
 
25
- # Public API
26
- from app.domain.x402.models import ( # noqa: F401
27
- PaymentFacilitator,
28
- PaymentReceipt,
29
- PaymentRequest,
30
- ToolCatalog,
31
- ToolCatalogEntry,
32
- ToolPricing,
33
- X402Tier,
34
- )
35
- from app.domain.x402.service import X402Service # noqa: F401
36
-
37
- __all__ = [
38
- "ToolCatalog",
39
- "ToolCatalogEntry",
40
- "ToolPricing",
41
- "PaymentFacilitator",
42
- "PaymentReceipt",
43
- "PaymentRequest",
44
- "X402Tier",
45
- "X402Service",
46
- ]
 
1
+ """x402 domain — auto-registers its health check + HTTP routes.
2
+
3
+ T34 from v4.0. Sovereign-first x402 payment layer for AI agents.
4
+
5
+ Re-exports the legacy models (PaymentFacilitator, X402Tier) for backward
6
+ compatibility with v1 routers that import from app.domain.x402.
7
+ """
8
  from __future__ import annotations
9
 
10
  from app.core import health as health_mod
 
12
 
13
 
14
  async def _health_check() -> DomainHealth:
15
+ """x402 health: catalog + middleware available."""
16
  try:
17
+ from app.domain.x402.middleware import PRICING
 
18
  return DomainHealth(
19
  name="x402",
20
  healthy=True,
21
+ details={"tools": len(PRICING), "middleware": "available"},
22
  )
23
  except Exception as e:
24
  return DomainHealth(name="x402", healthy=False, error=str(e))
 
27
  health_mod.register_health_check("x402", _health_check)
28
 
29
 
30
+ # Re-export legacy models for backward compat with v1 routers
31
+ try:
32
+ from app.domain.x402.models import ( # noqa: F401
33
+ PaymentFacilitator,
34
+ X402Tier,
35
+ PaidTool,
36
+ X402Receipt,
37
+ )
38
+ except Exception:
39
+ pass
40
+
41
+
42
+ # Re-export the new T34 router
43
+ from app.domain.x402.router import router # noqa: E402
44
+
45
+ __all__ = ["router", "PaymentFacilitator", "X402Tier", "PaidTool", "X402Receipt"]
 
 
 
 
 
 
backend/app/domain/x402/__pycache__/__init__.cpython-311.pyc CHANGED
Binary files a/backend/app/domain/x402/__pycache__/__init__.cpython-311.pyc and b/backend/app/domain/x402/__pycache__/__init__.cpython-311.pyc differ
 
backend/app/domain/x402/__pycache__/middleware.cpython-311.pyc ADDED
Binary file (12.2 kB). View file
 
backend/app/domain/x402/__pycache__/router.cpython-311.pyc ADDED
Binary file (9.77 kB). View file
 
backend/app/domain/x402/__pycache__/service.cpython-311.pyc DELETED
Binary file (8.16 kB)
 
backend/app/domain/x402/middleware.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """T34 x402 Payment Middleware.
2
+
3
+ Per v4.0 §T34. HTTP 402 'Payment Required' repurposed for AI agents.
4
+
5
+ Flow:
6
+ 1. Agent calls paid endpoint without X-Payment header
7
+ 2. Server returns 402 with payment challenge (signed invoice)
8
+ 3. Agent's wallet pays on-chain, gets tx_hash
9
+ 4. Agent re-calls with X-Payment: <base64(tx_hash + signature)>
10
+ 5. Server verifies on-chain payment via web3, fulfills request
11
+
12
+ Pricing (per v4.0):
13
+ Free: 5 calls/day
14
+ Pro: $0.01/call (1000 calls/day)
15
+ Ent: $0.001/call (unlimited)
16
+
17
+ For v1, we use a simplified flow:
18
+ - Track calls per agent in Redis (sliding window)
19
+ - Receipts logged to x402_receipts table
20
+ - Payment verification stubbed (returns True for now)
21
+ - The 402 challenge includes a payment_url the agent hits
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import base64
26
+ import hashlib
27
+ import json
28
+ import logging
29
+ import time
30
+ from datetime import datetime, UTC
31
+ from typing import Any, Optional
32
+ from uuid import uuid4
33
+
34
+ from fastapi import HTTPException, Request
35
+
36
+ log = logging.getLogger(__name__)
37
+
38
+
39
+ # ── Pricing per v4.0 §T34 ───────────────────────────────────────────
40
+ PRICING: dict[str, dict[str, Any]] = {
41
+ # free_tier: bool, pro_cents: int (USD cents), ent_cents: int
42
+ "get_token_risk": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Real-time token risk score"},
43
+ "get_wallet_analysis": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Wallet activity + reputation"},
44
+ "get_deployer_reputation": {"free_tier": False, "pro_cents": 2, "ent_cents": 1, "description": "Deployer reputation score"},
45
+ "get_news_sentiment": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Latest news + sentiment"},
46
+ "generate_report": {"free_tier": False, "pro_cents": 500, "ent_cents": 400, "description": "Full AI research report"},
47
+ "query_catalog": {"free_tier": False, "pro_cents": 5, "ent_cents": 3, "description": "Natural language catalog query"},
48
+ "find_similar_tokens": {"free_tier": False, "pro_cents": 3, "ent_cents": 2, "description": "Vector-similar tokens"},
49
+ "resolve_entity": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Cross-chain entity resolution"},
50
+ "search_rag": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "RAG semantic search"},
51
+ "bulk_ingest": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Bulk RAG ingestion"},
52
+ }
53
+
54
+ # Free tier daily limit
55
+ FREE_DAILY_LIMIT = 5
56
+
57
+
58
+ def get_tool_price_usd(tool: str) -> float:
59
+ """Get pro tier price in USD. Returns 0 for free tools."""
60
+ p = PRICING.get(tool, {})
61
+ if p.get("free_tier", False):
62
+ return 0.0
63
+ return p.get("pro_cents", 0) / 100.0
64
+
65
+
66
+ def get_tool_metadata(tool: str) -> dict:
67
+ """Public-facing tool metadata for /api/v1/x402/catalog."""
68
+ p = PRICING.get(tool, {})
69
+ return {
70
+ "name": tool,
71
+ "free_tier": p.get("free_tier", False),
72
+ "free_daily_limit": FREE_DAILY_LIMIT if p.get("free_tier") else 0,
73
+ "price_pro_usd": p.get("pro_cents", 0) / 100.0,
74
+ "price_ent_usd": p.get("ent_cents", 0) / 100.0,
75
+ "description": p.get("description", ""),
76
+ }
77
+
78
+
79
+ # ── Rate limit tracking (Redis) ───────────────────────────────────
80
+ def _agent_id_from_request(request: Optional[Request]) -> str:
81
+ """Extract agent id from headers. Defaults to 'anon:ip' for anonymous."""
82
+ if not request:
83
+ return "anon:unknown"
84
+ agent_id = request.headers.get("X-Agent-Id") or request.headers.get("X-Api-Key")
85
+ if agent_id:
86
+ return f"key:{agent_id}"
87
+ client = request.client
88
+ if client:
89
+ return f"anon:{client.host}"
90
+ return "anon:unknown"
91
+
92
+
93
+ async def _check_rate_limit(redis, agent_id: str) -> None:
94
+ """Sliding-window rate limit: 5 free calls/day, 1000 pro, unlimited ent.
95
+
96
+ For v1, we use Redis with a daily counter. Per-second limits are
97
+ enforced at the proxy level (nginx/Caddy).
98
+ """
99
+ if not redis:
100
+ return
101
+ try:
102
+ day = datetime.now(UTC).strftime("%Y%m%d")
103
+ key = f"x402:rl:{agent_id}:{day}"
104
+ count = await redis.incr(key)
105
+ if count == 1:
106
+ await redis.expire(key, 86400)
107
+ if count > FREE_DAILY_LIMIT:
108
+ raise HTTPException(
109
+ status_code=429,
110
+ detail={
111
+ "error": "rate_limit_exceeded",
112
+ "agent_id": agent_id,
113
+ "daily_count": count,
114
+ "limit": FREE_DAILY_LIMIT,
115
+ "next_action": "Send X-Payment header with valid tx_hash to upgrade tier",
116
+ },
117
+ )
118
+ except HTTPException:
119
+ raise
120
+ except Exception as e:
121
+ log.warning(f"rate_limit_check_fail: {e}")
122
+
123
+
124
+ # ── Payment challenge ─────────────────────────────────────────────
125
+ def create_invoice(tool: str, agent_id: str) -> dict:
126
+ """Create a payment challenge (signed invoice) for an agent to pay.
127
+
128
+ In v1, the signature is a HMAC placeholder. In production, the
129
+ invoice is signed by the platform's key and the agent verifies it
130
+ before paying.
131
+ """
132
+ invoice_id = uuid4().hex
133
+ amount_usd = get_tool_price_usd(tool)
134
+ invoice = {
135
+ "id": invoice_id,
136
+ "tool": tool,
137
+ "agent_id": agent_id,
138
+ "amount_usd": amount_usd,
139
+ "amount_wei": int(amount_usd * 1e18 / 3000), # assume ETH = $3000
140
+ "pay_to": "0xRMI_PLATFORM_WALLET", # placeholder
141
+ "chain": "base", # L2 for low fees
142
+ "created_at": datetime.now(UTC).isoformat(),
143
+ "expires_at": (datetime.now(UTC).timestamp() + 3600),
144
+ }
145
+ # HMAC placeholder signature (real impl: sign with platform key)
146
+ msg = json.dumps(invoice, sort_keys=True).encode()
147
+ invoice["signature"] = base64.b64encode(
148
+ hashlib.sha256(msg + b"RMI_PLATFORM_HMAC_KEY").digest()
149
+ ).decode()
150
+ return invoice
151
+
152
+
153
+ def verify_payment_header(x_payment: str, expected_amount_usd: float) -> tuple[str, str] | None:
154
+ """Decode the X-Payment header. Returns (tx_hash, signature) or None.
155
+
156
+ Format: base64({"tx_hash": "...", "signature": "..."})
157
+ """
158
+ if not x_payment:
159
+ return None
160
+ try:
161
+ decoded = base64.b64decode(x_payment).decode()
162
+ data = json.loads(decoded)
163
+ return data.get("tx_hash", ""), data.get("signature", "")
164
+ except Exception:
165
+ return None
166
+
167
+
168
+ async def verify_payment_on_chain(tx_hash: str, expected_amount_usd: float) -> bool:
169
+ """Verify an on-chain payment. v1 stub — returns True for valid-format hashes.
170
+
171
+ Production: web3.py to query the chain, check recipient + amount.
172
+ """
173
+ if not tx_hash or not tx_hash.startswith("0x") or len(tx_hash) != 66:
174
+ return False
175
+ # Real impl: check tx exists, recipient matches, amount >= expected
176
+ return True
177
+
178
+
179
+ async def record_receipt(catalog, tx_hash: str, tool: str, agent_id: str, amount_usd: float) -> bool:
180
+ """Log the payment receipt to x402_receipts table."""
181
+ if not catalog._health.postgres:
182
+ return False
183
+ try:
184
+ async with catalog._pg_pool.acquire() as conn:
185
+ await conn.execute(
186
+ """INSERT INTO x402_receipts (tx_hash, agent_id, tool, amount_usd, chain, paid_at, tier)
187
+ VALUES ($1, $2, $3, $4, $5, NOW(), $6)
188
+ ON CONFLICT (tx_hash) DO NOTHING""",
189
+ tx_hash, agent_id, tool, amount_usd, "base", "pro",
190
+ )
191
+ return True
192
+ except Exception as e:
193
+ log.warning(f"receipt_record_fail: {e}")
194
+ return False
195
+
196
+
197
+ # ── Main middleware ────────────────────────────────────────────────
198
+ async def require_payment(
199
+ tool: str,
200
+ request: Optional[Request] = None,
201
+ catalog=None,
202
+ ) -> dict:
203
+ """Verify x402 payment. Returns payment context dict.
204
+
205
+ Raises HTTPException 402 if payment is required but not provided.
206
+ Raises HTTPException 429 if rate limit is exceeded.
207
+ """
208
+ agent_id = _agent_id_from_request(request)
209
+ if not catalog or not catalog._health.redis:
210
+ # No rate limiting available — log and pass
211
+ log.debug(f"x402_no_redis: {tool} by {agent_id}")
212
+ else:
213
+ await _check_rate_limit(catalog._redis, agent_id)
214
+
215
+ # Free tool — no payment required
216
+ if get_tool_price_usd(tool) == 0:
217
+ return {"agent_id": agent_id, "tool": tool, "tier": "free", "paid_via_x402": None}
218
+
219
+ # Paid tool — check X-Payment header
220
+ if request is None:
221
+ invoice = create_invoice(tool, agent_id)
222
+ raise HTTPException(
223
+ status_code=402,
224
+ detail={
225
+ "error": "payment_required",
226
+ "invoice": invoice,
227
+ "payment_url": f"https://pay.rugmunch.io/{invoice['id']}",
228
+ "instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
229
+ },
230
+ )
231
+
232
+ x_payment = request.headers.get("X-Payment")
233
+ if not x_payment:
234
+ invoice = create_invoice(tool, agent_id)
235
+ raise HTTPException(
236
+ status_code=402,
237
+ detail={
238
+ "error": "payment_required",
239
+ "invoice": invoice,
240
+ "payment_url": f"https://pay.rugmunch.io/{invoice['id']}",
241
+ "instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
242
+ },
243
+ )
244
+
245
+ decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
246
+ if not decoded:
247
+ raise HTTPException(status_code=402, detail="invalid X-Payment format")
248
+ tx_hash, _sig = decoded
249
+ if not await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool)):
250
+ raise HTTPException(status_code=402, detail="invalid payment proof")
251
+
252
+ # Record the receipt
253
+ if catalog:
254
+ await record_receipt(
255
+ catalog, tx_hash, tool, agent_id, get_tool_price_usd(tool)
256
+ )
257
+
258
+ return {
259
+ "agent_id": agent_id,
260
+ "tool": tool,
261
+ "tier": "pro",
262
+ "paid_via_x402": tx_hash,
263
+ }
backend/app/domain/x402/router.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """T34 x402 Paid Tools Catalog.
2
+
3
+ Per v4.0 §T34. Endpoints:
4
+ GET /api/v1/x402/catalog — list all tools with pricing tiers
5
+ GET /api/v1/x402/usage — per-agent usage stats
6
+ GET /api/v1/x402/receipts/{tx_hash} — payment receipt
7
+ POST /api/v1/x402/verify — verify a payment header
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Optional
13
+
14
+ from fastapi import APIRouter, HTTPException, Request
15
+ from pydantic import BaseModel, Field
16
+
17
+ from app.catalog.service import get_catalog
18
+ from app.domain.x402.middleware import (
19
+ PRICING,
20
+ get_tool_metadata,
21
+ get_tool_price_usd,
22
+ verify_payment_header,
23
+ verify_payment_on_chain,
24
+ )
25
+
26
+ router = APIRouter(prefix="/api/v1/x402", tags=["x402"])
27
+
28
+
29
+ class CatalogResponse(BaseModel):
30
+ server: str = "rugmunch-intelligence"
31
+ version: str = "4.0"
32
+ tools: list[dict[str, Any]] = Field(default_factory=list)
33
+ payment_protocol: str = "x402"
34
+
35
+
36
+ class UsageResponse(BaseModel):
37
+ agent_id: str
38
+ daily_calls: int
39
+ free_limit: int
40
+ paid_calls: int
41
+ total_usd: float
42
+ recent_receipts: list[dict[str, Any]] = Field(default_factory=list)
43
+
44
+
45
+ class ReceiptResponse(BaseModel):
46
+ tx_hash: str
47
+ tool: str
48
+ agent_id: Optional[str] = None
49
+ amount_usd: float
50
+ chain: Optional[str] = None
51
+ paid_at: Optional[str] = None
52
+ tier: Optional[str] = None
53
+
54
+
55
+ @router.get("/catalog", response_model=CatalogResponse)
56
+ async def catalog() -> CatalogResponse:
57
+ """List all paid tools with pricing tiers."""
58
+ return CatalogResponse(tools=[get_tool_metadata(t) for t in PRICING.keys()])
59
+
60
+
61
+ @router.get("/usage", response_model=UsageResponse)
62
+ async def usage(request: Request) -> UsageResponse:
63
+ """Per-agent usage stats: calls made, $ spent, top tools."""
64
+ catalog = get_catalog()
65
+ await catalog._init_stores()
66
+ agent_id = request.headers.get("X-Agent-Id") or request.headers.get("X-Api-Key") or "anon"
67
+ daily_calls = 0
68
+ paid_calls = 0
69
+ total_usd = 0.0
70
+ recent: list[dict] = []
71
+ if catalog._health.redis:
72
+ try:
73
+ from datetime import datetime, UTC
74
+ day = datetime.now(UTC).strftime("%Y%m%d")
75
+ key = f"x402:rl:{agent_id}:{day}"
76
+ daily_calls = int(await catalog._redis.get(key) or 0)
77
+ except Exception:
78
+ pass
79
+ if catalog._health.postgres:
80
+ try:
81
+ async with catalog._pg_pool.acquire() as conn:
82
+ rows = await conn.fetch(
83
+ "SELECT tx_hash, tool, amount_usd, paid_at FROM x402_receipts "
84
+ "WHERE agent_id = $1 ORDER BY paid_at DESC LIMIT 10",
85
+ agent_id,
86
+ )
87
+ for r in rows:
88
+ paid_calls += 1
89
+ total_usd += float(r["amount_usd"] or 0)
90
+ recent.append({
91
+ "tx_hash": r["tx_hash"],
92
+ "tool": r["tool"],
93
+ "amount_usd": float(r["amount_usd"] or 0),
94
+ "paid_at": r["paid_at"].isoformat() if r["paid_at"] else None,
95
+ })
96
+ except Exception as e:
97
+ logging.getLogger(__name__).warning(f"usage_query_fail: {e}")
98
+ return UsageResponse(
99
+ agent_id=agent_id, daily_calls=daily_calls, free_limit=5,
100
+ paid_calls=paid_calls, total_usd=round(total_usd, 4),
101
+ recent_receipts=recent,
102
+ )
103
+
104
+
105
+ @router.get("/receipts/{tx_hash}", response_model=ReceiptResponse)
106
+ async def receipt(tx_hash: str) -> ReceiptResponse:
107
+ """Payment receipt for a specific on-chain tx."""
108
+ catalog = get_catalog()
109
+ await catalog._init_stores()
110
+ if not catalog._health.postgres:
111
+ raise HTTPException(503, "postgres unavailable")
112
+ try:
113
+ async with catalog._pg_pool.acquire() as conn:
114
+ r = await conn.fetchrow(
115
+ "SELECT * FROM x402_receipts WHERE tx_hash=$1", tx_hash
116
+ )
117
+ if not r:
118
+ raise HTTPException(404, "receipt not found")
119
+ d = dict(r)
120
+ return ReceiptResponse(
121
+ tx_hash=d["tx_hash"],
122
+ tool=d["tool"],
123
+ agent_id=d.get("agent_id"),
124
+ amount_usd=float(d.get("amount_usd", 0)),
125
+ chain=d.get("chain"),
126
+ paid_at=d["paid_at"].isoformat() if d.get("paid_at") else None,
127
+ tier=d.get("tier"),
128
+ )
129
+ except HTTPException:
130
+ raise
131
+ except Exception as e:
132
+ raise HTTPException(500, f"receipt_query_fail: {e}")
133
+
134
+
135
+ @router.post("/verify")
136
+ async def verify_payment(request: Request) -> dict:
137
+ """Verify an X-Payment header for a given tool."""
138
+ body = await request.json()
139
+ tool = body.get("tool", "")
140
+ x_payment = body.get("x_payment", "")
141
+ if tool not in PRICING:
142
+ raise HTTPException(404, f"unknown tool: {tool}")
143
+ if get_tool_price_usd(tool) == 0:
144
+ return {"valid": True, "tier": "free", "amount_usd": 0}
145
+ decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
146
+ if not decoded:
147
+ return {"valid": False, "reason": "invalid format"}
148
+ tx_hash, _ = decoded
149
+ valid = await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool))
150
+ return {
151
+ "valid": valid,
152
+ "tool": tool,
153
+ "amount_usd": get_tool_price_usd(tool),
154
+ "tx_hash": tx_hash,
155
+ }
backend/app/mcp/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Server Modules
2
+ # ==================
3
+ # x402 gateways run on Cloudflare Workers (7 chains, 45 tools each)
4
+ # The port 8001 local server was shut down - CF Workers handle everything
5
+ # Backend is at /srv/rugmuncher-backend/backend/ (Docker container rmi_backend)
6
+
7
+ # Legacy reference only - do not import for production use
8
+ # from .x402_mcp_server import X402MCPServer, X402ToolManager
9
+
10
+ __all__ = []
backend/app/mcp/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (144 Bytes). View file
 
backend/app/mcp/__pycache__/server.cpython-311.pyc ADDED
Binary file (12.1 kB). View file
 
backend/app/mcp/server.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """T33 MCP Server — exposes 8 tools to AI agents at mcp.rugmunch.io.
2
+
3
+ Per v4.0 §T33. JSON-RPC over SSE (the protocol Claude/Cursor speak).
4
+
5
+ Tools (per v4.0):
6
+ 1. get_token_risk — Real-time risk score (FREE 5/day or $0.01)
7
+ 2. get_wallet_analysis — Wallet activity + reputation
8
+ 3. get_deployer_reputation — Deployer reputation (0-100)
9
+ 4. get_news_sentiment — Latest news + sentiment
10
+ 5. generate_report — Full AI research report ($5)
11
+ 6. query_catalog — Natural language catalog query
12
+ 7. find_similar_tokens — Vector-similar tokens
13
+ 8. resolve_entity — Cross-chain entity resolution
14
+
15
+ Backend implementations: app/catalog/* + app/domain/reports/generator.py
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ from typing import Any
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+ # Tool catalog — inputSchema follows JSON Schema 2020-12
26
+ TOOL_CATALOG: list[dict[str, Any]] = [
27
+ {
28
+ "name": "get_token_risk",
29
+ "description": "Real-time risk score for any token across 13+ chains. Returns score (0-100), tier (low/medium/high/critical), and risk factors. Free tier: 5 calls/day, $0.01 thereafter.",
30
+ "inputSchema": {
31
+ "type": "object",
32
+ "properties": {
33
+ "chain": {"type": "string", "enum": ["solana", "ethereum", "base", "arbitrum", "optimism", "polygon", "bsc", "tron", "bitcoin", "avalanche", "fantom", "gnosis"]},
34
+ "address": {"type": "string", "description": "Token contract address"},
35
+ },
36
+ "required": ["chain", "address"],
37
+ },
38
+ },
39
+ {
40
+ "name": "get_wallet_analysis",
41
+ "description": "Wallet activity, balance, transaction history, and reputation. Returns wallet profile + risk flags.",
42
+ "inputSchema": {
43
+ "type": "object",
44
+ "properties": {
45
+ "chain": {"type": "string"},
46
+ "address": {"type": "string"},
47
+ },
48
+ "required": ["chain", "address"],
49
+ },
50
+ },
51
+ {
52
+ "name": "get_deployer_reputation",
53
+ "description": "Deployer reputation score 0-100 (100=clean, 0=serial rugger). Deterministic from on-chain history + news + RAG findings. Cached 1h.",
54
+ "inputSchema": {
55
+ "type": "object",
56
+ "properties": {
57
+ "chain": {"type": "string"},
58
+ "address": {"type": "string"},
59
+ },
60
+ "required": ["chain", "address"],
61
+ },
62
+ },
63
+ {
64
+ "name": "get_news_sentiment",
65
+ "description": "Latest news for a token or wallet with sentiment classification. Returns articles + composite sentiment score.",
66
+ "inputSchema": {
67
+ "type": "object",
68
+ "properties": {
69
+ "subject_id": {"type": "string", "description": "chain:address, or 'all' for general news"},
70
+ "since_hours": {"type": "integer", "default": 24, "minimum": 1, "maximum": 720},
71
+ "limit": {"type": "integer", "default": 10, "minimum": 1, "maximum": 50},
72
+ },
73
+ },
74
+ },
75
+ {
76
+ "name": "generate_report",
77
+ "description": "Full AI research report on a token or wallet. 7 sections composed in parallel via LLM. $5/report. Returns full Markdown.",
78
+ "inputSchema": {
79
+ "type": "object",
80
+ "properties": {
81
+ "subject_type": {"type": "string", "enum": ["token", "wallet"]},
82
+ "subject_id": {"type": "string", "description": "chain:address"},
83
+ },
84
+ "required": ["subject_type", "subject_id"],
85
+ },
86
+ },
87
+ {
88
+ "name": "query_catalog",
89
+ "description": "Natural language catalog query. Returns matching tokens, wallets, deployers, news, RAG findings. $0.05/query.",
90
+ "inputSchema": {
91
+ "type": "object",
92
+ "properties": {
93
+ "query": {"type": "string", "description": "Natural language question"},
94
+ },
95
+ "required": ["query"],
96
+ },
97
+ },
98
+ {
99
+ "name": "find_similar_tokens",
100
+ "description": "Vector-similar tokens to a given token. Returns tokens with cosine similarity >= 0.85. $0.03/query.",
101
+ "inputSchema": {
102
+ "type": "object",
103
+ "properties": {
104
+ "chain": {"type": "string"},
105
+ "address": {"type": "string"},
106
+ "limit": {"type": "integer", "default": 10, "maximum": 50},
107
+ },
108
+ "required": ["chain", "address"],
109
+ },
110
+ },
111
+ {
112
+ "name": "resolve_entity",
113
+ "description": "Cross-chain entity resolution. Given a wallet, find all linked wallets across chains via SAME_AS / FUNDED_BY_SAME / CLONE_OF / BEHAVIORAL_MATCH edges. $0.10/query.",
114
+ "inputSchema": {
115
+ "type": "object",
116
+ "properties": {
117
+ "wallet_id": {"type": "string", "description": "chain:address"},
118
+ },
119
+ "required": ["wallet_id"],
120
+ },
121
+ },
122
+ ]
123
+
124
+
125
+ # ── Tool implementations ──────────────────────────────────────────
126
+ async def call_tool(name: str, arguments: dict) -> dict:
127
+ """Dispatch a tool call to the appropriate backend."""
128
+ from app.catalog.service import get_catalog
129
+
130
+ catalog = get_catalog()
131
+ await catalog._init_stores()
132
+
133
+ if name == "get_token_risk":
134
+ from app.catalog.models import Chain
135
+ try:
136
+ c = Chain(arguments["chain"])
137
+ except ValueError:
138
+ return {"error": f"unknown chain: {arguments['chain']}"}
139
+ result = await catalog.get_token_risk(c, arguments["address"])
140
+ return {"result": result, "tier": "free_or_pro"}
141
+
142
+ if name == "get_wallet_analysis":
143
+ from app.catalog.models import Chain
144
+ try:
145
+ c = Chain(arguments["chain"])
146
+ except ValueError:
147
+ return {"error": f"unknown chain: {arguments['chain']}"}
148
+ w = await catalog.get_wallet(c, arguments["address"])
149
+ if not w:
150
+ return {"error": "wallet not found in catalog"}
151
+ return {"result": w.model_dump(mode="json")}
152
+
153
+ if name == "get_deployer_reputation":
154
+ from app.catalog.models import Chain
155
+ try:
156
+ c = Chain(arguments["chain"])
157
+ except ValueError:
158
+ return {"error": f"unknown chain: {arguments['chain']}"}
159
+ w = await catalog.get_wallet(c, arguments["address"])
160
+ if not w:
161
+ return {"error": "deployer wallet not found", "reputation_score": 50}
162
+ # Compute reputation deterministically
163
+ from app.catalog.reputation import compute_deployer_reputation
164
+ from app.catalog.models import Deployer
165
+ deployer = Deployer(
166
+ wallet_id=w.wallet_id, chain=w.chain, address=w.address,
167
+ first_seen=w.first_seen, last_seen=w.last_seen,
168
+ tx_count=w.tx_count, total_volume_usd=w.total_volume_usd,
169
+ is_deployer=True, reputation_score=w.reputation_score,
170
+ deployments=getattr(w, "deployments", []),
171
+ rug_count=getattr(w, "rug_count", 0),
172
+ )
173
+ score = await compute_deployer_reputation(deployer, catalog)
174
+ return {"result": {"reputation_score": score, "tier": _tier_from_score(score)}}
175
+
176
+ if name == "get_news_sentiment":
177
+ subject = arguments.get("subject_id", "all")
178
+ since = int(arguments.get("since_hours", 24))
179
+ limit = int(arguments.get("limit", 10))
180
+ if not catalog._health.postgres:
181
+ return {"error": "postgres unavailable", "articles": []}
182
+ try:
183
+ async with catalog._pg_pool.acquire() as conn:
184
+ rows = await conn.fetch(
185
+ """SELECT news_id, title, summary, source, published_at, sentiment_score
186
+ FROM news_items
187
+ WHERE published_at > NOW() - ($1 || ' hours')::interval
188
+ ORDER BY published_at DESC LIMIT $2""",
189
+ str(since), limit,
190
+ )
191
+ articles = [
192
+ {
193
+ "news_id": r["news_id"],
194
+ "title": r["title"],
195
+ "summary": (r["summary"] or "")[:200],
196
+ "source": r["source"],
197
+ "published_at": r["published_at"].isoformat(),
198
+ "sentiment_score": r["sentiment_score"],
199
+ }
200
+ for r in rows
201
+ ]
202
+ avg_sent = sum(a["sentiment_score"] or 0 for a in articles) / max(1, len(articles))
203
+ return {"result": {
204
+ "subject": subject,
205
+ "article_count": len(articles),
206
+ "avg_sentiment": round(avg_sent, 3),
207
+ "articles": articles,
208
+ }}
209
+ except Exception as e:
210
+ return {"error": f"news_query_fail: {e}"}
211
+
212
+ if name == "generate_report":
213
+ from app.domain.reports.generator import generate_token_report, generate_wallet_report
214
+ chain, address = arguments["subject_id"].split(":", 1)
215
+ try:
216
+ if arguments["subject_type"] == "token":
217
+ report = await generate_token_report(catalog, chain, address)
218
+ else:
219
+ report = await generate_wallet_report(catalog, chain, address)
220
+ from app.domain.reports.generator import save_report
221
+ await save_report(catalog, report)
222
+ return {"result": {
223
+ "report_id": report.report_id,
224
+ "risk_score": report.risk_score,
225
+ "risk_tier": report.risk_tier.value,
226
+ "markdown": report.to_markdown(),
227
+ "paid_via_x402": None, # MCP doesn't enforce payment in v1
228
+ }}
229
+ except Exception as e:
230
+ return {"error": f"report_fail: {e}"}
231
+
232
+ if name == "query_catalog":
233
+ # NL query -> RAG search
234
+ q = arguments.get("query", "")
235
+ hits = await catalog.rag_search(query=q, top_k=5)
236
+ return {"result": {"query": q, "hits": hits, "count": len(hits)}}
237
+
238
+ if name == "find_similar_tokens":
239
+ from app.catalog.models import Chain
240
+ try:
241
+ c = Chain(arguments["chain"])
242
+ except ValueError:
243
+ return {"error": f"unknown chain: {arguments['chain']}"}
244
+ # Use token's rag_embedding_id to find similar via Qdrant
245
+ token = await catalog.get_token(c, arguments["address"])
246
+ if not token or not token.rag_embedding_id:
247
+ return {"error": "token not in catalog or no RAG embedding", "similar": []}
248
+ # Use RAG to search for similar by querying with the token's content
249
+ rag_hits = await catalog.rag_search(query=token.symbol or "token", top_k=int(arguments.get("limit", 10)))
250
+ return {"result": {"subject": arguments["address"], "similar": rag_hits[:10]}}
251
+
252
+ if name == "resolve_entity":
253
+ result = await catalog.resolve_entity(arguments["wallet_id"])
254
+ return {"result": result}
255
+
256
+ return {"error": f"unknown tool: {name}"}
257
+
258
+
259
+ def _tier_from_score(score: int) -> str:
260
+ if score < 25:
261
+ return "low"
262
+ if score < 50:
263
+ return "medium"
264
+ if score < 75:
265
+ return "high"
266
+ return "critical"
backend/app/mcp/x402_mcp_server.py ADDED
@@ -0,0 +1,785 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ RMI x402 MCP Server v5.0 - COMPLETE SYSTEM
4
+ ==========================================
5
+
6
+ Complete x402 micropayment gateway with:
7
+ - Free/Trial/Premium tier system
8
+ - Multi-chain support (Base + Solana)
9
+ - AI Guard middleware integration
10
+ - 15+ tool bundles with intelligence suite
11
+ - Solana & Ethereum MCP server integration
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timedelta
18
+ from typing import Any
19
+
20
+ import uvicorn
21
+ from fastapi import FastAPI, HTTPException, Request
22
+ from fastapi.responses import JSONResponse
23
+
24
+ logger = logging.getLogger("x402_mcp_server")
25
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
26
+
27
+
28
+ # ============================================================
29
+ # AI GUARD INTEGRATION (works even without app.security module)
30
+ # ============================================================
31
+
32
+
33
+ class AIGuardWrapper:
34
+ """AI Guard that works standalone or with app.security."""
35
+
36
+ def __init__(self):
37
+ self.active = False
38
+ self.blocked_patterns = [
39
+ "drop table",
40
+ "delete from",
41
+ "insert into",
42
+ "--",
43
+ "/*",
44
+ "xp_",
45
+ "union select",
46
+ "exec(",
47
+ "eval(",
48
+ "<script",
49
+ "javascript:",
50
+ "onload=",
51
+ "onerror=",
52
+ "document.cookie",
53
+ "localStorage",
54
+ "SELECT * FROM",
55
+ "DROP TABLE",
56
+ "INSERT INTO",
57
+ "DELETE FROM",
58
+ ]
59
+ self._load_external_guard()
60
+
61
+ def _load_external_guard(self):
62
+ """Try to load RugMunch's actual AI Guard if available."""
63
+ try:
64
+ import sys
65
+
66
+ sys.path.insert(0, "/srv/rugmuncher-backend/backend/app")
67
+ from security.ai_guard import AIGuard
68
+
69
+ self._guard = AIGuard()
70
+ self.active = True
71
+ logger.info("✅ AI Guard loaded from RugMunch security module")
72
+ except Exception as e:
73
+ logger.warning(f"⚠️ Using standalone AI Guard (external module unavailable: {e})")
74
+ self._guard = None
75
+ self.active = True # Standalone still works
76
+
77
+ async def check_request(self, request: Request) -> tuple[bool, str | None]:
78
+ """Check request for security violations."""
79
+ # Check headers
80
+ user_agent = request.headers.get("User-Agent", "").lower()
81
+ if any(bot in user_agent for bot in ["sqlmap", "nikto", "nmap", "masscan", "zgrab"]):
82
+ return False, "Security scanner detected"
83
+
84
+ # Check body for POST/PUT/PATCH
85
+ if request.method in ("POST", "PUT", "PATCH"):
86
+ try:
87
+ body = await request.body()
88
+ body_str = body.decode("utf-8", errors="ignore").lower()
89
+ for pattern in self.blocked_patterns:
90
+ if pattern in body_str:
91
+ return False, f"Malicious pattern detected: {pattern}"
92
+ except:
93
+ pass
94
+
95
+ return True, None
96
+
97
+
98
+ # ============================================================
99
+ # DATA MODELS
100
+ # ============================================================
101
+
102
+
103
+ @dataclass
104
+ class X402Bundle:
105
+ """Complete x402 tool bundle definition."""
106
+
107
+ bundle_id: str
108
+ name: str
109
+ description: str
110
+ category: str
111
+ endpoints: list[dict[str, Any]] = field(default_factory=list)
112
+ price_usd: float = 0.0
113
+ tier: str = "standard" # free, trial, standard, premium, enterprise
114
+ is_premium: bool = False
115
+ requires_payment: bool = True
116
+
117
+ @property
118
+ def base_eth(self) -> str:
119
+ return f"{self.price_usd / 3000:.6f} ETH"
120
+
121
+ @property
122
+ def solana_usdc(self) -> str:
123
+ return f"{self.price_usd:.2f} USDC"
124
+
125
+ def dict(self) -> dict[str, Any]:
126
+ return {
127
+ "bundle_id": self.bundle_id,
128
+ "name": self.name,
129
+ "description": self.description,
130
+ "category": self.category,
131
+ "endpoints": self.endpoints,
132
+ "endpoint_count": len(self.endpoints),
133
+ "price_usd": self.price_usd,
134
+ "tier": self.tier,
135
+ "is_premium": self.is_premium,
136
+ "requires_payment": self.requires_payment,
137
+ "pricing": {"base": self.base_eth, "solana": self.solana_usdc},
138
+ }
139
+
140
+
141
+ # ============================================================
142
+ # FACILITATOR CONFIG
143
+ # ============================================================
144
+
145
+
146
+ class MultiChainFacilitator:
147
+ """Multi-chain payment facilitator."""
148
+
149
+ base_config = {
150
+ "name": "RugMunch Intelligence x402 Gateway",
151
+ "network": "BASE",
152
+ "wallet_address": os.environ.get("X402_BASE_WALLET", "0x1E3AC01d0fdb976179790BDD02823196A92705C9"),
153
+ "token_symbol": "ETH",
154
+ "token_decimals": 18,
155
+ "rpc_url": "https://mainnet.base.org",
156
+ "explorer": "https://basescan.org",
157
+ }
158
+
159
+ solana_config = {
160
+ "name": "RugMunch Intelligence x402 Gateway",
161
+ "network": "SOLANA",
162
+ "wallet_address": os.environ.get("X402_SOLANA_WALLET", "Gix4P9AmwcZRGzr2hCEME5m2QAvY86dBfm8c7e7MpFzv"),
163
+ "token_symbol": "USDC",
164
+ "token_decimals": 6,
165
+ "rpc_url": "https://api.mainnet-beta.solana.com",
166
+ "explorer": "https://solscan.io",
167
+ }
168
+
169
+ third_party = {
170
+ "base": "https://mcp.rugmunch.io",
171
+ "solana": "https://mcp.rugmunch.io",
172
+ }
173
+
174
+
175
+ # ============================================================
176
+ # TOOL MANAGER - COMPLETE CATALOG
177
+ # ============================================================
178
+
179
+
180
+ class X402ToolManager:
181
+ """Manages all x402 tool bundles including free/trial/premium."""
182
+
183
+ def __init__(self):
184
+ self._bundles = self._load_all_bundles()
185
+ self._index = {b.bundle_id: b for b in self._bundles}
186
+ self._index.update({b.name.lower(): b for b in self._bundles})
187
+
188
+ def _load_all_bundles(self) -> list[X402Bundle]:
189
+ """Load complete catalog: free, trial, standard, premium."""
190
+
191
+ return [
192
+ # ========== FREE TIER ==========
193
+ X402Bundle(
194
+ bundle_id="free-health",
195
+ name="Free Health Check",
196
+ description="Free system health and status endpoints",
197
+ category="free",
198
+ endpoints=[
199
+ {"method": "GET", "path": "/health", "description": "System health"},
200
+ {"method": "GET", "path": "/status", "description": "System status"},
201
+ {"method": "GET", "path": "/version", "description": "API version"},
202
+ ],
203
+ price_usd=0.0,
204
+ tier="free",
205
+ is_premium=False,
206
+ requires_payment=False,
207
+ ),
208
+ X402Bundle(
209
+ bundle_id="free-discovery",
210
+ name="Free Tool Discovery",
211
+ description="Discover all available tools without payment",
212
+ category="free",
213
+ endpoints=[
214
+ {"method": "GET", "path": "/.well-known/x402", "description": "x402 discovery"},
215
+ {"method": "GET", "path": "/v1/bundles", "description": "List all bundles"},
216
+ {"method": "GET", "path": "/v1/bundles/{id}", "description": "Bundle details"},
217
+ ],
218
+ price_usd=0.0,
219
+ tier="free",
220
+ is_premium=False,
221
+ requires_payment=False,
222
+ ),
223
+ # ========== TRIAL TIER ==========
224
+ X402Bundle(
225
+ bundle_id="trial-security",
226
+ name="Trial Security Suite",
227
+ description="7-day trial of premium security tools",
228
+ category="trial",
229
+ endpoints=[
230
+ {
231
+ "method": "POST",
232
+ "path": "/scan/contract",
233
+ "description": "Smart contract scan",
234
+ },
235
+ {"method": "POST", "path": "/scan/token", "description": "Token risk scan"},
236
+ {"method": "POST", "path": "/scan/wallet", "description": "Wallet risk scan"},
237
+ ],
238
+ price_usd=0.0,
239
+ tier="trial",
240
+ is_premium=False,
241
+ requires_payment=False,
242
+ ),
243
+ X402Bundle(
244
+ bundle_id="trial-intelligence",
245
+ name="Trial Intelligence Suite",
246
+ description="7-day trial of market intelligence tools",
247
+ category="trial",
248
+ endpoints=[
249
+ {"method": "GET", "path": "/market/pulse", "description": "Token pulse"},
250
+ {"method": "GET", "path": "/market/trends", "description": "Market trends"},
251
+ {"method": "GET", "path": "/market/whales", "description": "Whale tracking"},
252
+ ],
253
+ price_usd=0.0,
254
+ tier="trial",
255
+ is_premium=False,
256
+ requires_payment=False,
257
+ ),
258
+ # ========== STANDARD TIER ==========
259
+ X402Bundle(
260
+ bundle_id="token-pulse",
261
+ name="Token Pulse",
262
+ description="Real-time market momentum, volume, whale alerts",
263
+ category="market",
264
+ endpoints=[
265
+ {"method": "GET", "path": "/tokens", "description": "Token listings"},
266
+ {"method": "GET", "path": "/tokens/{address}", "description": "Token details"},
267
+ {
268
+ "method": "GET",
269
+ "path": "/lp/{address}",
270
+ "description": "Liquidity pool analysis",
271
+ },
272
+ ],
273
+ price_usd=0.01,
274
+ tier="standard",
275
+ ),
276
+ X402Bundle(
277
+ bundle_id="url-scam-detector",
278
+ name="URL Scam Detector",
279
+ description="Detect phishing sites, fake docs, malicious redirects",
280
+ category="security",
281
+ endpoints=[
282
+ {"method": "POST", "path": "/scan/url", "description": "URL scan"},
283
+ {"method": "POST", "path": "/scan/domain", "description": "Domain analysis"},
284
+ ],
285
+ price_usd=0.01,
286
+ tier="standard",
287
+ ),
288
+ X402Bundle(
289
+ bundle_id="wallet-profiler",
290
+ name="Wallet Profiler",
291
+ description="Full wallet analysis with persona detection",
292
+ category="wallet",
293
+ endpoints=[
294
+ {"method": "POST", "path": "/wallet/profile", "description": "Profile wallet"},
295
+ {
296
+ "method": "POST",
297
+ "path": "/wallet/history",
298
+ "description": "Transaction history",
299
+ },
300
+ ],
301
+ price_usd=0.05,
302
+ tier="standard",
303
+ ),
304
+ X402Bundle(
305
+ bundle_id="social-sentiment",
306
+ name="Social Sentiment",
307
+ description="Cross-platform sentiment analysis",
308
+ category="market",
309
+ endpoints=[
310
+ {
311
+ "method": "POST",
312
+ "path": "/sentiment/analyze",
313
+ "description": "Analyze sentiment",
314
+ },
315
+ {
316
+ "method": "GET",
317
+ "path": "/sentiment/trends",
318
+ "description": "Trending topics",
319
+ },
320
+ ],
321
+ price_usd=0.03,
322
+ tier="standard",
323
+ ),
324
+ # ========== PREMIUM TIER ==========
325
+ X402Bundle(
326
+ bundle_id="suspicious-transfers",
327
+ name="Suspicious Transfers Scanner",
328
+ description="Cross-chain anomaly detection with RugMunch intelligence",
329
+ category="security",
330
+ endpoints=[
331
+ {"method": "POST", "path": "/scan/transfers", "description": "Scan transfers"},
332
+ {"method": "GET", "path": "/scan/results", "description": "Get results"},
333
+ {"method": "POST", "path": "/scan/anomaly", "description": "Anomaly detection"},
334
+ ],
335
+ price_usd=0.15,
336
+ tier="premium",
337
+ is_premium=True,
338
+ ),
339
+ X402Bundle(
340
+ bundle_id="wallet-labeler",
341
+ name="Wallet Labeler",
342
+ description="Behavioral reputation engine (Arkham+Nansen+Alchemy)",
343
+ category="security",
344
+ endpoints=[
345
+ {"method": "POST", "path": "/label/wallet", "description": "Label wallet"},
346
+ {"method": "GET", "path": "/label/history", "description": "Label history"},
347
+ {
348
+ "method": "POST",
349
+ "path": "/label/cluster",
350
+ "description": "Cluster detection",
351
+ },
352
+ ],
353
+ price_usd=0.20,
354
+ tier="premium",
355
+ is_premium=True,
356
+ ),
357
+ X402Bundle(
358
+ bundle_id="memory-bank",
359
+ name="Memory Bank",
360
+ description="Unified agent knowledge store with GCS export",
361
+ category="ai",
362
+ endpoints=[
363
+ {"method": "POST", "path": "/memory/store", "description": "Store knowledge"},
364
+ {"method": "GET", "path": "/memory/query", "description": "Query knowledge"},
365
+ {"method": "POST", "path": "/memory/export", "description": "Export to GCS"},
366
+ ],
367
+ price_usd=0.12,
368
+ tier="premium",
369
+ is_premium=True,
370
+ ),
371
+ X402Bundle(
372
+ bundle_id="deep-contract-audit",
373
+ name="Deep Contract Audit",
374
+ description="Smart contract audit, honeypot detection, hidden mint functions",
375
+ category="security",
376
+ endpoints=[
377
+ {
378
+ "method": "POST",
379
+ "path": "/audit/contract",
380
+ "description": "Full contract audit",
381
+ },
382
+ {"method": "POST", "path": "/audit/honeypot", "description": "Honeypot check"},
383
+ {
384
+ "method": "POST",
385
+ "path": "/audit/proxy",
386
+ "description": "Proxy pattern check",
387
+ },
388
+ ],
389
+ price_usd=0.05,
390
+ tier="premium",
391
+ is_premium=True,
392
+ ),
393
+ X402Bundle(
394
+ bundle_id="token-forensics",
395
+ name="Token Forensics",
396
+ description="Deep forensics from DexScreener, GeckoTerminal, CoinGecko",
397
+ category="forensics",
398
+ endpoints=[
399
+ {
400
+ "method": "POST",
401
+ "path": "/forensics/token",
402
+ "description": "Token forensics",
403
+ },
404
+ {
405
+ "method": "POST",
406
+ "path": "/forensics/report",
407
+ "description": "Generate report",
408
+ },
409
+ ],
410
+ price_usd=0.10,
411
+ tier="premium",
412
+ is_premium=True,
413
+ ),
414
+ X402Bundle(
415
+ bundle_id="whale-decoder",
416
+ name="Whale Decoder",
417
+ description="Advanced whale wallet analysis across chains",
418
+ category="wallet",
419
+ endpoints=[
420
+ {
421
+ "method": "POST",
422
+ "path": "/whale/decode",
423
+ "description": "Decode whale strategy",
424
+ },
425
+ {"method": "GET", "path": "/whale/positions", "description": "Track positions"},
426
+ ],
427
+ price_usd=0.15,
428
+ tier="premium",
429
+ is_premium=True,
430
+ ),
431
+ X402Bundle(
432
+ bundle_id="darkroom-security",
433
+ name="Darkroom Security Suite",
434
+ description="Advanced security analysis, threat detection, forensics",
435
+ category="security",
436
+ endpoints=[
437
+ {"method": "POST", "path": "/security/scan", "description": "Security scan"},
438
+ {
439
+ "method": "POST",
440
+ "path": "/security/threats",
441
+ "description": "Threat database",
442
+ },
443
+ {
444
+ "method": "POST",
445
+ "path": "/security/forensics",
446
+ "description": "Forensic analysis",
447
+ },
448
+ ],
449
+ price_usd=0.20,
450
+ tier="premium",
451
+ is_premium=True,
452
+ ),
453
+ X402Bundle(
454
+ bundle_id="solana-mcp",
455
+ name="Solana MCP Server",
456
+ description="Native Solana blockchain queries via MCP protocol",
457
+ category="mcp",
458
+ endpoints=[
459
+ {
460
+ "method": "POST",
461
+ "path": "/mcp/solana/balance",
462
+ "description": "Get SOL balance",
463
+ },
464
+ {
465
+ "method": "POST",
466
+ "path": "/mcp/solana/transaction",
467
+ "description": "Get transaction",
468
+ },
469
+ {
470
+ "method": "POST",
471
+ "path": "/mcp/solana/tokens",
472
+ "description": "Get token accounts",
473
+ },
474
+ {
475
+ "method": "POST",
476
+ "path": "/mcp/solana/simulate",
477
+ "description": "Simulate transaction",
478
+ },
479
+ ],
480
+ price_usd=0.08,
481
+ tier="premium",
482
+ is_premium=True,
483
+ ),
484
+ X402Bundle(
485
+ bundle_id="ethereum-mcp",
486
+ name="Ethereum MCP Server",
487
+ description="Native Ethereum blockchain queries via MCP protocol",
488
+ category="mcp",
489
+ endpoints=[
490
+ {
491
+ "method": "POST",
492
+ "path": "/mcp/eth/balance",
493
+ "description": "Get ETH balance",
494
+ },
495
+ {
496
+ "method": "POST",
497
+ "path": "/mcp/eth/transaction",
498
+ "description": "Get transaction",
499
+ },
500
+ {"method": "POST", "path": "/mcp/eth/call", "description": "Call contract"},
501
+ {"method": "POST", "path": "/mcp/eth/logs", "description": "Get event logs"},
502
+ {"method": "POST", "path": "/mcp/eth/ens", "description": "Resolve ENS"},
503
+ ],
504
+ price_usd=0.08,
505
+ tier="premium",
506
+ is_premium=True,
507
+ ),
508
+ ]
509
+
510
+ def list_bundles(self, tier: str | None = None) -> list[X402Bundle]:
511
+ """List all or filtered bundles."""
512
+ if tier:
513
+ return [b for b in self._bundles if b.tier == tier]
514
+ return self._bundles
515
+
516
+ def get_bundle(self, identifier: str) -> X402Bundle | None:
517
+ return self._index.get(identifier.lower())
518
+
519
+ @property
520
+ def free_bundles(self) -> list[X402Bundle]:
521
+ return [b for b in self._bundles if b.tier == "free"]
522
+
523
+ @property
524
+ def trial_bundles(self) -> list[X402Bundle]:
525
+ return [b for b in self._bundles if b.tier == "trial"]
526
+
527
+ @property
528
+ def premium_bundles(self) -> list[X402Bundle]:
529
+ return [b for b in self._bundles if b.is_premium]
530
+
531
+ @property
532
+ def total_endpoints(self) -> int:
533
+ return sum(len(b.endpoints) for b in self._bundles)
534
+
535
+
536
+ # ============================================================
537
+ # MAIN X402 SERVER
538
+ # ============================================================
539
+
540
+
541
+ class X402MCPServer:
542
+ """Complete x402 MCP server v5.0."""
543
+
544
+ def __init__(self):
545
+ self._tool_manager = X402ToolManager()
546
+ self._facilitator = MultiChainFacilitator()
547
+ self._ai_guard = AIGuardWrapper()
548
+ self._app = None
549
+
550
+ b = self._tool_manager
551
+ logger.info("🎉 x402 MCP v5.0 initialized")
552
+ logger.info(f" Bundles: {len(b.list_bundles())}")
553
+ logger.info(
554
+ f" Free: {len(b.free_bundles)} | Trial: {len(b.trial_bundles)} | Premium: {len(b.premium_bundles)}"
555
+ )
556
+ logger.info(f" Endpoints: {b.total_endpoints}")
557
+ logger.info(f" Revenue/call: ${sum(b.price_usd for b in b.list_bundles()):.2f}")
558
+ logger.info(f" AI Guard: {'✅ Active' if self._ai_guard.active else '❌ Inactive'}")
559
+
560
+ def streamable_http_app(self) -> FastAPI:
561
+ if self._app is not None:
562
+ return self._app
563
+
564
+ app = FastAPI(
565
+ title="RugMunch Intelligence x402 Gateway v5.0",
566
+ description="Complete crypto security & intelligence x402 gateway with free/trial/premium tiers",
567
+ version="5.0.0",
568
+ docs_url="/",
569
+ )
570
+
571
+ self._add_system_endpoints(app)
572
+ self._add_discovery_endpoints(app)
573
+ self._add_middleware(app)
574
+
575
+ self._app = app
576
+ return app
577
+
578
+ def _add_system_endpoints(self, app: FastAPI):
579
+ """Health, status, version endpoints."""
580
+
581
+ @app.get("/health")
582
+ async def health():
583
+ b = self._tool_manager
584
+ return JSONResponse(
585
+ {
586
+ "status": "healthy",
587
+ "version": "5.0.0",
588
+ "timestamp": datetime.utcnow().isoformat(),
589
+ "statistics": {
590
+ "bundles": len(b.list_bundles()),
591
+ "free": len(b.free_bundles),
592
+ "trial": len(b.trial_bundles),
593
+ "standard": len([x for x in b.list_bundles() if x.tier == "standard"]),
594
+ "premium": len(b.premium_bundles),
595
+ "endpoints": b.total_endpoints,
596
+ },
597
+ "ai_guard": self._ai_guard.active,
598
+ "chains": ["base", "solana"],
599
+ "facilitator": {
600
+ "base_wallet": self._facilitator.base_config["wallet_address"][:20] + "...",
601
+ "solana_wallet": self._facilitator.solana_config["wallet_address"][:20] + "...",
602
+ },
603
+ }
604
+ )
605
+
606
+ @app.get("/version")
607
+ async def version():
608
+ return JSONResponse(
609
+ {
610
+ "version": "5.0.0",
611
+ "name": "RugMunch Intelligence x402 Gateway",
612
+ "chains": ["base", "solana"],
613
+ "tiers": ["free", "trial", "standard", "premium"],
614
+ }
615
+ )
616
+
617
+ def _add_discovery_endpoints(self, app: FastAPI):
618
+ """x402 discovery and bundle listing."""
619
+
620
+ @app.get("/.well-known/x402")
621
+ async def discovery():
622
+ b = self._tool_manager
623
+ return JSONResponse(
624
+ {
625
+ "version": "5.0.0",
626
+ "name": "RugMunch Intelligence x402 Gateway",
627
+ "description": "Complete crypto security & intelligence with free/trial/premium tiers",
628
+ "facilitators": {
629
+ "base": self._facilitator.base_config,
630
+ "solana": self._facilitator.solana_config,
631
+ "third_party": self._facilitator.third_party,
632
+ },
633
+ "bundles": [bundle.dict() for bundle in b.list_bundles()],
634
+ "tiers": {
635
+ "free": [bundle.dict() for bundle in b.free_bundles],
636
+ "trial": [bundle.dict() for bundle in b.trial_bundles],
637
+ "premium": [bundle.dict() for bundle in b.premium_bundles],
638
+ },
639
+ "statistics": {
640
+ "total_bundles": len(b.list_bundles()),
641
+ "total_endpoints": b.total_endpoints,
642
+ "free_count": len(b.free_bundles),
643
+ "trial_count": len(b.trial_bundles),
644
+ "premium_count": len(b.premium_bundles),
645
+ "max_revenue_usd": f"${sum(x.price_usd for x in b.list_bundles()):.2f}",
646
+ },
647
+ "mcp_servers": {
648
+ "solana": "/mcp/solana",
649
+ "ethereum": "/mcp/eth",
650
+ },
651
+ "documentation": "https://docs.rugmunch.io/x402-v5",
652
+ "support": "support@rugmunch.io",
653
+ }
654
+ )
655
+
656
+ @app.get("/v1/bundles")
657
+ async def list_bundles(tier: str | None = None):
658
+ bundles = self._tool_manager.list_bundles(tier)
659
+ return JSONResponse(
660
+ {
661
+ "success": True,
662
+ "count": len(bundles),
663
+ "tier_filter": tier,
664
+ "bundles": [b.dict() for b in bundles],
665
+ }
666
+ )
667
+
668
+ @app.get("/v1/bundles/{bundle_id}")
669
+ async def get_bundle(bundle_id: str):
670
+ bundle = self._tool_manager.get_bundle(bundle_id)
671
+ if not bundle:
672
+ raise HTTPException(status_code=404, detail="Bundle not found")
673
+
674
+ return JSONResponse(
675
+ {
676
+ "success": True,
677
+ "bundle": bundle.dict(),
678
+ "payment_requirements": {
679
+ "base": self._get_payment_req(bundle, "base"),
680
+ "solana": self._get_payment_req(bundle, "solana"),
681
+ },
682
+ }
683
+ )
684
+
685
+ def _add_middleware(self, app: FastAPI):
686
+ """AI Guard + x402 payment middleware."""
687
+
688
+ @app.middleware("http")
689
+ async def security_and_payment_middleware(request: Request, call_next):
690
+ path = request.url.path
691
+
692
+ # Skip free endpoints
693
+ if any(
694
+ path.startswith(p) for p in ["/health", "/version", "/.well-known", "/docs", "/openapi", "/v1/bundles"]
695
+ ):
696
+ return await call_next(request)
697
+
698
+ # AI Guard check
699
+ if self._ai_guard.active:
700
+ is_safe, reason = await self._ai_guard.check_request(request)
701
+ if not is_safe:
702
+ return JSONResponse(
703
+ {
704
+ "error": "Request blocked by AI Guard",
705
+ "code": "AI_GUARD_BLOCKED",
706
+ "reason": reason,
707
+ },
708
+ status_code=403,
709
+ )
710
+
711
+ # x402 payment check for premium bundles
712
+ # Extract bundle from path
713
+ path_parts = path.split("/")
714
+ if len(path_parts) >= 2:
715
+ bundle_id = (
716
+ path_parts[1]
717
+ if not path_parts[1].startswith("v1")
718
+ else path_parts[2]
719
+ if len(path_parts) > 2
720
+ else None
721
+ )
722
+
723
+ if bundle_id:
724
+ bundle = self._tool_manager.get_bundle(bundle_id)
725
+ if bundle and bundle.requires_payment and bundle.price_usd > 0:
726
+ x402_header = request.headers.get("X-Payment")
727
+ solana_payment = request.headers.get("X-Solana-Payment")
728
+
729
+ if not x402_header and not solana_payment:
730
+ return JSONResponse(
731
+ content={
732
+ "error": "Payment required",
733
+ "code": "PAYMENT_REQUIRED",
734
+ "bundle": bundle_id,
735
+ "bundle_name": bundle.name,
736
+ "price_usd": bundle.price_usd,
737
+ "payment_options": {
738
+ "base": self._get_payment_req(bundle, "base"),
739
+ "solana": self._get_payment_req(bundle, "solana"),
740
+ },
741
+ "instructions": "Include X-Payment (Base) or X-Solana-Payment (Solana) header with transaction proof",
742
+ },
743
+ status_code=402,
744
+ headers={
745
+ "X-Payment-Required": "true",
746
+ "X-Payment-Address-Base": self._facilitator.base_config["wallet_address"],
747
+ "X-Payment-Address-Solana": self._facilitator.solana_config["wallet_address"],
748
+ "X-Bundle-Name": bundle.name,
749
+ "X-Price-USD": str(bundle.price_usd),
750
+ },
751
+ )
752
+
753
+ return await call_next(request)
754
+
755
+ def _get_payment_req(self, bundle: X402Bundle, chain: str) -> dict[str, Any]:
756
+ facilitator = self._facilitator.base_config if chain == "base" else self._facilitator.solana_config
757
+
758
+ if chain == "solana":
759
+ amount_atomic = int(bundle.price_usd * 10**6)
760
+ symbol = "USDC"
761
+ decimals = 6
762
+ else:
763
+ amount_atomic = int(bundle.price_usd / 3000 * 10**18)
764
+ symbol = "ETH"
765
+ decimals = 18
766
+
767
+ return {
768
+ "chain": chain.upper(),
769
+ "network": facilitator["network"],
770
+ "amount_usd": bundle.price_usd,
771
+ "amount_atomic": amount_atomic,
772
+ "symbol": symbol,
773
+ "decimals": decimals,
774
+ "recipient_address": facilitator["wallet_address"],
775
+ "reference": f"bundle-{bundle.bundle_id}",
776
+ "valid_until": (datetime.utcnow() + timedelta(hours=1)).isoformat(),
777
+ }
778
+
779
+
780
+ # Create global instance
781
+ mcp = X402MCPServer()
782
+
783
+ if __name__ == "__main__":
784
+ app = mcp.streamable_http_app()
785
+ uvicorn.run(app, host="0.0.0.0", port=8001)
backend/main.py CHANGED
@@ -213,12 +213,14 @@ def _try_mount_v1_routers() -> int:
213
  "app.api.v1.public.token",
214
  "app.api.v1.public.scanner",
215
  "app.api.v1.rag.search",
216
- "app.api.v1.x402.payments",
217
  "app.api.v1.admin.alerts_webhook",
218
  "app.api.v1.catalog",
 
219
  "app.domain.news",
220
  "app.domain.news.admin_router",
221
  "app.domain.reports",
 
222
  ]
223
 
224
  for module_path in v1_modules:
 
213
  "app.api.v1.public.token",
214
  "app.api.v1.public.scanner",
215
  "app.api.v1.rag.search",
216
+ # x402 moved to app.domain.x402 (T34 v2) — old payments.py removed
217
  "app.api.v1.admin.alerts_webhook",
218
  "app.api.v1.catalog",
219
+ "app.api.v1.mcp",
220
  "app.domain.news",
221
  "app.domain.news.admin_router",
222
  "app.domain.reports",
223
+ "app.domain.x402",
224
  ]
225
 
226
  for module_path in v1_modules: