merge: chore/cleanup-remove-bloat-and-secrets into main

This commit is contained in:
Crypto Rug Munch 2026-07-02 01:24:22 +07:00
commit bde2f3a97d
1173 changed files with 437609 additions and 0 deletions

View file

@ -0,0 +1,53 @@
"""x402 domain — auto-registers its health check + HTTP routes.
T34 from v4.0. Sovereign-first x402 payment layer for AI agents.
Re-exports the legacy models (PaymentFacilitator, X402Tier) for backward
compatibility with v1 routers that import from app.domain.x402.
"""
from __future__ import annotations
from app.core import health as health_mod
from app.core.health import DomainHealth
async def _health_check() -> DomainHealth:
"""x402 health: catalog + middleware available."""
try:
from app.domain.x402.middleware import PRICING
return DomainHealth(
name="x402",
healthy=True,
details={"tools": len(PRICING), "middleware": "available"},
)
except Exception as e:
return DomainHealth(name="x402", healthy=False, error=str(e))
health_mod.register_health_check("x402", _health_check)
# Re-export models for backward compat with v1 routers and tests
from app.domain.x402.models import (
PaymentFacilitator,
PaymentReceipt,
ToolCatalog,
ToolCatalogEntry,
ToolPricing,
X402Tier,
)
# Re-export the new T34 router
from app.domain.x402.router import router # noqa: E402
from app.domain.x402.service import X402Service
__all__ = [
"PaymentFacilitator",
"PaymentReceipt",
"ToolCatalog",
"ToolCatalogEntry",
"ToolPricing",
"X402Service",
"X402Tier",
"router",
]

View file

@ -0,0 +1,449 @@
"""T34 x402 Payment Middleware.
Per v4.0 §T34. HTTP 402 'Payment Required' repurposed for AI agents.
Flow:
1. Agent calls paid endpoint without X-Payment header
2. Server returns 402 with payment challenge (signed invoice)
3. Agent's wallet pays on-chain, gets tx_hash
4. Agent re-calls with X-Payment: <base64(tx_hash + signature)>
5. Server verifies on-chain payment via web3, fulfills request
Pricing (per v4.0):
Free: 5 calls/day
Pro: $0.01/call (1000 calls/day)
Ent: $0.001/call (unlimited)
For v1, we use a simplified flow:
- Track calls per agent in Redis (sliding window)
- Receipts logged to x402_receipts table
- Payment verification stubbed (returns True for now)
- The 402 challenge includes a payment_url the agent hits
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import logging
import os
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
from fastapi import HTTPException, Request
log = logging.getLogger(__name__)
# ── Pricing per v4.0 §T34 ───────────────────────────────────────────
PRICING: dict[str, dict[str, Any]] = {
# free_tier: bool, pro_cents: int (USD cents), ent_cents: int
"get_token_risk": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Real-time token risk score"},
"get_wallet_analysis": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Wallet activity + reputation"},
"get_deployer_reputation": {"free_tier": False, "pro_cents": 2, "ent_cents": 1, "description": "Deployer reputation score"},
"get_news_sentiment": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "Latest news + sentiment"},
"generate_report": {"free_tier": False, "pro_cents": 500, "ent_cents": 400, "description": "Full AI research report"},
"query_catalog": {"free_tier": False, "pro_cents": 5, "ent_cents": 3, "description": "Natural language catalog query"},
"find_similar_tokens": {"free_tier": False, "pro_cents": 3, "ent_cents": 2, "description": "Vector-similar tokens"},
"resolve_entity": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Cross-chain entity resolution"},
"search_rag": {"free_tier": True, "pro_cents": 1, "ent_cents": 1, "description": "RAG semantic search"},
"bulk_ingest": {"free_tier": False, "pro_cents": 10, "ent_cents": 5, "description": "Bulk RAG ingestion"},
}
# Free tier daily limit
FREE_DAILY_LIMIT = 5
# ── Nonce burn (replay protection) ──────────────────────────────
# Per MINIMAX_M3_TASKS.md T02. Each tx_hash can only be used ONCE.
# We use Redis SETNX (set-if-not-exists) as an atomic nonce burn:
# - First call: SETNX returns 1, nonce is burned, request proceeds
# - Second call: SETNX returns 0, nonce already burned → 410 Gone
# TTL is 24h (the x402 invoice expiry window).
X402_NONCE_TTL = 86400 # 24 hours
async def _burn_nonce(catalog, tx_hash: str) -> bool:
"""Atomically burn a payment nonce. Returns True if first use, False if replay.
Uses Redis SETNX (SET key value NX EX seconds). Atomic by construction.
Logs replay attempts for monitoring.
"""
if not catalog or not catalog._health.redis:
# Without Redis we cannot prevent replay — fail closed (reject).
log.warning(f"x402_nocache_reject: tx_hash={tx_hash[:10]}...")
return False
key = f"x402:nonce:{tx_hash}"
try:
# SETNX semantics: return 1 if key was set, 0 if already existed
ok = await catalog._redis.set(key, "1", ex=X402_NONCE_TTL, nx=True)
if not ok:
log.warning(f"x402_replay_attempt: tx_hash={tx_hash[:10]}...")
_X402_REPLAY_COUNTER.labels(tool="unknown").inc()
return bool(ok)
except Exception as e:
log.error(f"x402_nonce_burn_fail: {e}")
# Fail closed — reject if we can't burn atomically
return False
# ── Prometheus counters (lightweight, no external dep) ──────────
try:
from prometheus_client import REGISTRY, Counter
# Use a module-level registry so multiple imports don't double-register
_X402_REGISTRY = REGISTRY
_X402_REPLAY_COUNTER = Counter(
"x402_replay_attempts_total",
"Number of x402 replay attempts blocked by nonce burn",
["tool"],
registry=_X402_REGISTRY,
)
_X402_REVENUE_COUNTER = Counter(
"x402_revenue_total_usd_cents",
"x402 revenue in USD cents",
["tool", "tier"],
registry=_X402_REGISTRY,
)
except Exception:
# Prometheus not available — use no-op counter stubs
class _Stub:
def labels(self, **kw): return self
def inc(self, n: float = 1.0): pass
_X402_REPLAY_COUNTER = _Stub()
_X402_REVENUE_COUNTER = _Stub()
def get_tool_price_usd(tool: str) -> float:
"""Get the per-call price for a tool in USD.
Checks PRICING dict first, then falls back to the enforcement
system's TOOL_PRICES for full coverage of 270+ tools.
Returns 0.01 as default if tool is not found in either.
"""
p = PRICING.get(tool, {})
if p.get("free_tier", False):
return 0.0
if p.get("pro_cents"):
return p["pro_cents"] / 100.0
# Fallback: check the enforcement system's comprehensive price list
try:
from routers.x402_enforcement import TOOL_PRICES as _FALLBACK_PRICES, _ensure_tool_prices
_ensure_tool_prices()
fp = _FALLBACK_PRICES.get(tool, {})
return fp.get("price_usd", 0.01)
except Exception:
return p.get("pro_cents", 1) / 100.0
def get_tool_metadata(tool: str) -> dict:
"""Public-facing tool metadata for /api/v1/x402/catalog."""
p = PRICING.get(tool, {})
return {
"name": tool,
"free_tier": p.get("free_tier", False),
"free_daily_limit": FREE_DAILY_LIMIT if p.get("free_tier") else 0,
"price_pro_usd": p.get("pro_cents", 0) / 100.0,
"price_ent_usd": p.get("ent_cents", 0) / 100.0,
"description": p.get("description", ""),
}
# ── Rate limit tracking (Redis) ───────────────────────────────────
def _agent_id_from_request(request: Request | None) -> str:
"""Extract agent id from headers. Defaults to 'anon:ip' for anonymous.
Security: X-Agent-Id is self-declared and marked as unverified.
X-Api-Key is checked against our admin key if configured.
"""
if not request:
return "anon:unknown"
api_key = request.headers.get("X-Api-Key", "")
if api_key:
# Verify against configured admin key
admin_key = os.getenv("ADMIN_API_KEY", os.getenv("WP_ADMIN_KEY", ""))
if admin_key and api_key == admin_key:
return "key:admin"
return f"unverified_key:{api_key[:16]}"
agent_id = request.headers.get("X-Agent-Id", "")
if agent_id:
return f"unverified_agent:{agent_id[:20]}"
client = request.client
if client:
return f"anon:{client.host}"
return "anon:unknown"
async def _check_rate_limit(redis, agent_id: str) -> None:
"""Sliding-window rate limit: 5 free calls/day, 1000 pro, unlimited ent.
For v1, we use Redis with a daily counter. Per-second limits are
enforced at the proxy level (nginx/Caddy).
"""
if not redis:
return
try:
day = datetime.now(UTC).strftime("%Y%m%d")
key = f"x402:rl:{agent_id}:{day}"
count = await redis.incr(key)
if count == 1:
await redis.expire(key, 86400)
if count > FREE_DAILY_LIMIT:
raise HTTPException(
status_code=429,
detail={
"error": "rate_limit_exceeded",
"agent_id": agent_id,
"daily_count": count,
"limit": FREE_DAILY_LIMIT,
"next_action": "Send X-Payment header with valid tx_hash to upgrade tier",
},
)
except HTTPException:
raise
except Exception as e:
log.warning(f"rate_limit_check_fail: {e}")
# ── HMAC key from env ────────────────────────────────────────────
def _get_hmac_key() -> bytes:
"""Load HMAC signing key from environment. Never hardcoded."""
key = os.getenv("X402_HMAC_KEY", "")
if not key:
key = os.getenv("ADMIN_API_KEY", os.getenv("WP_ADMIN_KEY", "dev-key-only-for-local"))
if "dev" not in key:
logging.getLogger("wp.x402").warning("X402_HMAC_KEY not set, using ADMIN_API_KEY")
return key.encode()
# ── Payment challenge ─────────────────────────────────────────────
def create_invoice(tool: str, agent_id: str) -> dict:
"""Create a payment challenge (signed invoice) for an agent to pay.
The invoice is signed with the platform's HMAC key (from env var
X402_HMAC_KEY, fallback ADMIN_API_KEY). The agent can verify the
signature before paying.
"""
invoice_id = uuid4().hex
amount_usd = get_tool_price_usd(tool)
pay_to = os.getenv("X402_PAYMENT_ADDRESS", "")
hmac_key = _get_hmac_key()
invoice = {
"id": invoice_id,
"tool": tool,
"agent_id": agent_id,
"amount_usd": amount_usd,
"amount_wei": int(amount_usd * 1e18 / float(os.getenv("X402_ETH_PRICE_USD", "3000"))) if amount_usd > 0 else 0,
"pay_to": pay_to or "0xRMI_PLATFORM_WALLET (set X402_PAYMENT_ADDRESS)",
"chain": os.getenv("X402_PAYMENT_CHAIN", "base"),
"created_at": datetime.now(UTC).isoformat(),
"expires_at": (datetime.now(UTC).timestamp() + 3600),
}
msg = json.dumps(invoice, sort_keys=True).encode()
invoice["signature"] = base64.b64encode(
hmac.new(hmac_key, msg, hashlib.sha256).digest()
).decode()
return invoice
def verify_payment_header(x_payment: str, expected_amount_usd: float) -> tuple[str, str] | None:
"""Decode the X-Payment header. Returns (tx_hash, signature) or None.
Format: base64({"tx_hash": "...", "signature": "..."})
"""
if not x_payment:
return None
try:
decoded = base64.b64decode(x_payment).decode()
data = json.loads(decoded)
return data.get("tx_hash", ""), data.get("signature", "")
except Exception:
return None
async def verify_payment_on_chain(tx_hash: str, expected_amount_usd: float) -> bool:
"""Verify an on-chain payment.
Checks via public RPC that the transaction:
1. Exists on-chain
2. Transferred >= expected_amount_usd of USDC
3. Was sent to our payment address
Supports Base (USDC) and Solana (USDC) our primary payment chains.
Falls back to the main x402_enforcement self-verify for other chains.
"""
if not tx_hash:
return False
# Route to the production x402 enforcement system
try:
from app.routers.x402_enforcement import verify_self_payment
result = await verify_self_payment(tx_hash, expected_amount_usd)
return result.get("verified", False)
except ImportError:
pass
except Exception as e:
logging.getLogger("wp.x402").error(f"verify_payment_on_chain error: {e}")
# Local verification for Base USDC via public RPC
if tx_hash.startswith("0x") and len(tx_hash) == 66:
try:
import httpx
pay_to = os.getenv("X402_PAYMENT_ADDRESS", "")
if not pay_to:
return False
async with httpx.AsyncClient(timeout=15) as c:
r = await c.post("https://mainnet.base.org", json={
"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionReceipt",
"params": [tx_hash],
})
data = r.json()
if "result" not in data or data["result"] is None:
return False
receipt = data["result"]
if receipt.get("status") != "0x1":
return False
from_address = receipt.get("from", "").lower()
to_address = receipt.get("to", "").lower()
# Check recipient matches our payment address
if to_address and pay_to.lower() != to_address:
return False
return True
except Exception as e:
logging.getLogger("wp.x402").error(f"Base tx verify failed: {e}")
return False
# Solana verification via public RPC
if len(tx_hash) == 88: # Solana base58 signature length
try:
import httpx
async with httpx.AsyncClient(timeout=15) as c:
r = await c.post("https://api.mainnet-beta.solana.com", json={
"jsonrpc": "2.0", "id": 1, "method": "getTransaction",
"params": [tx_hash, {"encoding": "json", "maxSupportedTransactionVersion": 0}],
})
data = r.json()
if "result" not in data or data["result"] is None:
return False
return True
except Exception as e:
logging.getLogger("wp.x402").error(f"Solana tx verify failed: {e}")
return False
return False
async def record_receipt(catalog, tx_hash: str, tool: str, agent_id: str, amount_usd: float) -> bool:
"""Log the payment receipt to x402_receipts table."""
if not catalog._health.postgres:
return False
try:
async with catalog._pg_pool.acquire() as conn:
await conn.execute(
"""INSERT INTO x402_receipts (tx_hash, agent_id, tool, amount_usd, chain, paid_at, tier)
VALUES ($1, $2, $3, $4, $5, NOW(), $6)
ON CONFLICT (tx_hash) DO NOTHING""",
tx_hash, agent_id, tool, amount_usd,
os.getenv("X402_PAYMENT_CHAIN", "base"),
os.getenv("X402_DEFAULT_TIER", "pro"),
)
return True
except Exception as e:
log.warning(f"receipt_record_fail: {e}")
return False
# ── Main middleware ────────────────────────────────────────────────
async def require_payment(
tool: str,
request: Request | None = None,
catalog=None,
) -> dict:
"""Verify x402 payment. Returns payment context dict.
Raises HTTPException 402 if payment is required but not provided.
Raises HTTPException 429 if rate limit is exceeded.
"""
agent_id = _agent_id_from_request(request)
if not catalog or not catalog._health.redis:
# No rate limiting available — log and pass
log.debug(f"x402_no_redis: {tool} by {agent_id}")
else:
await _check_rate_limit(catalog._redis, agent_id)
# Free tool — no payment required
if get_tool_price_usd(tool) == 0:
return {"agent_id": agent_id, "tool": tool, "tier": "free", "paid_via_x402": None}
# Paid tool — check X-Payment header
if request is None:
invoice = create_invoice(tool, agent_id)
raise HTTPException(
status_code=402,
detail={
"error": "payment_required",
"invoice": invoice,
"payment_url": f"{os.getenv('X402_PAYMENT_URL', 'https://pay.rugmunch.io')}/{invoice['id']}",
"instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
},
)
x_payment = request.headers.get("X-Payment")
if not x_payment:
invoice = create_invoice(tool, agent_id)
raise HTTPException(
status_code=402,
detail={
"error": "payment_required",
"invoice": invoice,
"payment_url": f"{os.getenv('X402_PAYMENT_URL', 'https://pay.rugmunch.io')}/{invoice['id']}",
"instructions": "Pay on-chain, then retry with X-Payment header (base64 of {tx_hash, signature})",
},
)
decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
if not decoded:
raise HTTPException(status_code=402, detail="invalid X-Payment format")
tx_hash, _sig = decoded
if not await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool)):
raise HTTPException(status_code=402, detail="invalid payment proof")
# T02: Burn the nonce atomically. Same tx_hash cannot be used twice.
if not await _burn_nonce(catalog, tx_hash):
raise HTTPException(
status_code=410,
detail={
"error": "payment_already_used",
"tx_hash": tx_hash,
"message": "This payment receipt has already been used. Each tx_hash can only be used once.",
},
)
_X402_REVENUE_COUNTER.labels(tool=tool, tier="pro").inc(int(get_tool_price_usd(tool) * 100))
# Record the receipt
if catalog:
await record_receipt(
catalog, tx_hash, tool, agent_id, get_tool_price_usd(tool)
)
return {
"agent_id": agent_id,
"tool": tool,
"tier": "pro",
"paid_via_x402": tx_hash,
}

86
app/domain/x402/models.py Normal file
View file

@ -0,0 +1,86 @@
"""Pydantic v2 models for the x402 domain."""
from __future__ import annotations
from enum import StrEnum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class X402Tier(StrEnum):
"""x402 payment tier — maps to scanner tiers."""
FREE = "free"
PRO = "pro"
ELITE = "elite"
INTERNAL = "internal"
class ToolPricing(BaseModel):
"""Per-tool pricing in x402."""
model_config = ConfigDict(str_strip_whitespace=True)
tool: str
price_usd: float = Field(default=0.0, ge=0)
price_crypto: dict[str, str] = Field(
default_factory=dict,
description="Crypto amounts: {'sol': '0.01', 'eth': '0.001', 'btc': '0.00001', 'trx': '5'}",
)
tier_required: X402Tier = X402Tier.FREE
class ToolCatalogEntry(BaseModel):
"""A single tool in the x402 catalog."""
name: str
description: str = ""
category: str = ""
tier_required: X402Tier = X402Tier.FREE
price_usd: float = 0.0
endpoint: str = ""
enabled: bool = True
class ToolCatalog(BaseModel):
"""Full x402 tool catalog."""
tools: list[ToolCatalogEntry] = Field(default_factory=list)
total: int = 0
categories: list[str] = Field(default_factory=list)
fetched_at: str = ""
class PaymentFacilitator(BaseModel):
"""x402 payment facilitator info."""
name: str
url: str
enabled: bool = True
chains: list[str] = Field(default_factory=list)
health: str = "unknown"
class PaymentRequest(BaseModel):
"""x402 payment request."""
model_config = ConfigDict(str_strip_whitespace=True)
tool: str
chain: str = Field(default="solana")
user_address: str | None = None
@field_validator("chain")
@classmethod
def _chain_lowercase(cls, v: str) -> str:
return v.lower().strip()
class PaymentReceipt(BaseModel):
"""x402 payment receipt."""
tool: str
chain: str
tx_hash: str | None = None
amount_paid: dict[str, str] = Field(default_factory=dict)
status: str = "pending" # pending | confirmed | failed
timestamp: str = ""

155
app/domain/x402/router.py Normal file
View file

@ -0,0 +1,155 @@
"""T34 x402 Paid Tools Catalog.
Per v4.0 §T34. Endpoints:
GET /api/v1/x402/catalog list all tools with pricing tiers
GET /api/v1/x402/usage per-agent usage stats
GET /api/v1/x402/receipts/{tx_hash} payment receipt
POST /api/v1/x402/verify verify a payment header
"""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from app.catalog.service import get_catalog
from app.domain.x402.middleware import (
PRICING,
get_tool_metadata,
get_tool_price_usd,
verify_payment_header,
verify_payment_on_chain,
)
router = APIRouter(prefix="/api/v1/x402", tags=["x402"])
class CatalogResponse(BaseModel):
server: str = "rugmunch-intelligence"
version: str = "4.0"
tools: list[dict[str, Any]] = Field(default_factory=list)
payment_protocol: str = "x402"
class UsageResponse(BaseModel):
agent_id: str
daily_calls: int
free_limit: int
paid_calls: int
total_usd: float
recent_receipts: list[dict[str, Any]] = Field(default_factory=list)
class ReceiptResponse(BaseModel):
tx_hash: str
tool: str
agent_id: str | None = None
amount_usd: float
chain: str | None = None
paid_at: str | None = None
tier: str | None = None
@router.get("/catalog", response_model=CatalogResponse)
async def catalog() -> CatalogResponse:
"""List all paid tools with pricing tiers."""
return CatalogResponse(tools=[get_tool_metadata(t) for t in PRICING])
@router.get("/usage", response_model=UsageResponse)
async def usage(request: Request) -> UsageResponse:
"""Per-agent usage stats: calls made, $ spent, top tools."""
catalog = get_catalog()
await catalog._init_stores()
agent_id = request.headers.get("X-Agent-Id") or request.headers.get("X-Api-Key") or "anon"
daily_calls = 0
paid_calls = 0
total_usd = 0.0
recent: list[dict] = []
if catalog._health.redis:
try:
from datetime import UTC, datetime
day = datetime.now(UTC).strftime("%Y%m%d")
key = f"x402:rl:{agent_id}:{day}"
daily_calls = int(await catalog._redis.get(key) or 0)
except Exception:
pass
if catalog._health.postgres:
try:
async with catalog._pg_pool.acquire() as conn:
rows = await conn.fetch(
"SELECT tx_hash, tool, amount_usd, paid_at FROM x402_receipts "
"WHERE agent_id = $1 ORDER BY paid_at DESC LIMIT 10",
agent_id,
)
for r in rows:
paid_calls += 1
total_usd += float(r["amount_usd"] or 0)
recent.append({
"tx_hash": r["tx_hash"],
"tool": r["tool"],
"amount_usd": float(r["amount_usd"] or 0),
"paid_at": r["paid_at"].isoformat() if r["paid_at"] else None,
})
except Exception as e:
logging.getLogger(__name__).warning(f"usage_query_fail: {e}")
return UsageResponse(
agent_id=agent_id, daily_calls=daily_calls, free_limit=5,
paid_calls=paid_calls, total_usd=round(total_usd, 4),
recent_receipts=recent,
)
@router.get("/receipts/{tx_hash}", response_model=ReceiptResponse)
async def receipt(tx_hash: str) -> ReceiptResponse:
"""Payment receipt for a specific on-chain tx."""
catalog = get_catalog()
await catalog._init_stores()
if not catalog._health.postgres:
raise HTTPException(503, "postgres unavailable")
try:
async with catalog._pg_pool.acquire() as conn:
r = await conn.fetchrow(
"SELECT * FROM x402_receipts WHERE tx_hash=$1", tx_hash
)
if not r:
raise HTTPException(404, "receipt not found")
d = dict(r)
return ReceiptResponse(
tx_hash=d["tx_hash"],
tool=d["tool"],
agent_id=d.get("agent_id"),
amount_usd=float(d.get("amount_usd", 0)),
chain=d.get("chain"),
paid_at=d["paid_at"].isoformat() if d.get("paid_at") else None,
tier=d.get("tier"),
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(500, f"receipt_query_fail: {e}")
@router.post("/verify")
async def verify_payment(request: Request) -> dict:
"""Verify an X-Payment header for a given tool."""
body = await request.json()
tool = body.get("tool", "")
x_payment = body.get("x_payment", "")
if tool not in PRICING:
raise HTTPException(404, f"unknown tool: {tool}")
if get_tool_price_usd(tool) == 0:
return {"valid": True, "tier": "free", "amount_usd": 0}
decoded = verify_payment_header(x_payment, get_tool_price_usd(tool))
if not decoded:
return {"valid": False, "reason": "invalid format"}
tx_hash, _ = decoded
valid = await verify_payment_on_chain(tx_hash, get_tool_price_usd(tool))
return {
"valid": valid,
"tool": tool,
"amount_usd": get_tool_price_usd(tool),
"tx_hash": tx_hash,
}

130
app/domain/x402/service.py Normal file
View file

@ -0,0 +1,130 @@
"""x402 service — facade over legacy x402 routers.
Calls into the existing x402 catalog/enforcement routers. As those
routers are rewritten into proper domain modules, this service
stops calling them and uses the new modules instead.
"""
from __future__ import annotations
import asyncio
from typing import Any
from app.core.logging import get_logger
from app.domain.x402.models import (
PaymentFacilitator,
ToolCatalog,
ToolCatalogEntry,
X402Tier,
)
log = get_logger(__name__)
class X402Service:
"""Async facade over the x402 payment system."""
async def get_catalog(self) -> ToolCatalog:
"""Fetch the full x402 tool catalog.
Delegates to app.routers.x402_catalog.list_tools_catalog() (the
legacy catalog endpoint function). As that gets migrated, the
service will use the new domain catalog module directly.
"""
log.info("x402_catalog_fetch_started")
try:
import app.routers.x402_catalog as _catalog_mod
raw = await _catalog_mod.list_tools_catalog()
return self._wrap_catalog(raw)
except Exception as e:
log.warning("x402_catalog_fetch_failed", error=str(e))
return ToolCatalog()
async def get_facilitators(self) -> list[PaymentFacilitator]:
"""Fetch list of enabled payment facilitators.
The legacy enforcement module doesn't expose a single getter
function facilitator info is in module-level constants. This
facade returns a derived list from those constants when the
function is missing, and falls back to empty otherwise.
"""
log.info("x402_facilitators_fetch_started")
try:
import app.routers.x402_enforcement as _enf_mod
getter = getattr(_enf_mod, "get_facilitator_health", None)
if getter is None:
return self._default_facilitators()
raw = getter()
if asyncio.iscoroutine(raw):
raw = await raw
return self._wrap_facilitators(raw)
except Exception as e:
log.warning("x402_facilitators_fetch_failed", error=str(e))
return self._default_facilitators()
@staticmethod
def _default_facilitators() -> list[PaymentFacilitator]:
"""Default facilitator list — matches the legacy enforcement config."""
return [
PaymentFacilitator(name="Coinbase CDP", url="https://api.cdp.coinbase.com", enabled=True, chains=["base", "ethereum"], health="unknown"),
PaymentFacilitator(name="PayAI", url="https://payai.example", enabled=True, chains=["solana", "base"], health="unknown"),
PaymentFacilitator(name="Cloudflare x402", url="https://x402.cloudflare.com", enabled=True, chains=["base", "polygon"], health="unknown"),
]
@staticmethod
def _wrap_catalog(raw: Any) -> ToolCatalog:
"""Convert legacy catalog response → Pydantic ToolCatalog."""
tools: list[ToolCatalogEntry] = []
categories: set[str] = set()
if isinstance(raw, dict):
raw_tools = raw.get("tools", [])
total = raw.get("total", len(raw_tools))
categories = set(raw.get("categories", []))
elif isinstance(raw, list):
raw_tools = raw
total = len(raw)
else:
return ToolCatalog()
for t in raw_tools:
if isinstance(t, dict):
try:
tier_str = t.get("tier_required", t.get("tier", "free"))
entry = ToolCatalogEntry(
name=t.get("name", t.get("tool", "")),
description=t.get("description", ""),
category=t.get("category", ""),
tier_required=X402Tier(tier_str.lower() if isinstance(tier_str, str) else "free"),
price_usd=float(t.get("price_usd", 0) or 0),
endpoint=t.get("endpoint", ""),
enabled=bool(t.get("enabled", True)),
)
tools.append(entry)
if entry.category:
categories.add(entry.category)
except Exception:
continue
return ToolCatalog(
tools=tools,
total=total,
categories=sorted(categories),
)
@staticmethod
def _wrap_facilitators(raw: Any) -> list[PaymentFacilitator]:
"""Convert legacy facilitator list → Pydantic list."""
out: list[PaymentFacilitator] = []
items: list[dict] = []
if isinstance(raw, dict):
items = raw.get("facilitators", raw.get("data", []))
elif isinstance(raw, list):
items = raw
for f in items:
if isinstance(f, dict):
out.append(PaymentFacilitator(
name=f.get("name", ""),
url=f.get("url", ""),
enabled=bool(f.get("enabled", True)),
chains=f.get("chains", []) or [],
health=f.get("health", "unknown"),
))
return out

View file

@ -0,0 +1,157 @@
"""x402 tools registry - main entry point for x402 tool implementations.
Per v4.0 §T26. Provides a unified interface to all x402-powered tools.
Tools are delegated to domain/x402/tools/*.py for organization.
Registry shape:
tools = {
"tool_name": {
"function": async function,
"free_tier": bool,
"pro_cents": int | None,
"ent_cents": int | None,
"description": str,
}
}
"""
from app.domain.x402.tools.label_tools import get_entity_labels, get_wallet_labels
from app.domain.x402.tools.market_tools import get_market_overview, get_trending
from app.domain.x402.tools.news_tools import get_news, get_news_sentiment
from app.domain.x402.tools.report_tools import generate_report, get_report
from app.domain.x402.tools.scanner_tools import get_scan_result, run_scan
from app.domain.x402.tools.token_tools import get_token_info, get_token_risk
from app.domain.x402.tools.wallet_tools import get_wallet_analysis, get_wallet_history
# Tool registry
TOOLS = {
"get_token_risk": {
"function": get_token_risk,
"free_tier": True,
"pro_cents": 50,
"ent_cents": 40,
"description": "Analyze token for rug pull indicators, honeypots, mintability",
},
"get_token_info": {
"function": get_token_info,
"free_tier": True,
"pro_cents": 10,
"ent_cents": 8,
"description": "Get basic token metadata (name, symbol, supply, holders)",
},
"get_wallet_analysis": {
"function": get_wallet_analysis,
"free_tier": False,
"pro_cents": 200,
"ent_cents": 150,
"description": "Deep wallet behavior analysis (PnL, trading patterns, personas)",
},
"get_wallet_history": {
"function": get_wallet_history,
"free_tier": False,
"pro_cents": 100,
"ent_cents": 80,
"description": "Get transaction history for a wallet",
},
"get_market_overview": {
"function": get_market_overview,
"free_tier": True,
"pro_cents": 50,
"ent_cents": 40,
"description": "Market overview for a chain (top tokens, volume, trend)",
},
"get_trending": {
"function": get_trending,
"free_tier": True,
"pro_cents": 30,
"ent_cents": 25,
"description": "Get trending tokens on a chain",
},
"run_scan": {
"function": run_scan,
"free_tier": True,
"pro_cents": 100,
"ent_cents": 80,
"description": "Run full SENTINEL scan on a token",
},
"get_scan_result": {
"function": get_scan_result,
"free_tier": False,
"pro_cents": 50,
"ent_cents": 40,
"description": "Get scan result for a token",
},
"generate_report": {
"function": generate_report,
"free_tier": False,
"pro_cents": 500,
"ent_cents": 400,
"description": "Full AI research report (7 sections, $5)",
},
"get_report": {
"function": get_report,
"free_tier": False,
"pro_cents": 200,
"ent_cents": 150,
"description": "Get previously generated report",
},
"get_news": {
"function": get_news,
"free_tier": True,
"pro_cents": 30,
"ent_cents": 25,
"description": "Get news mentions for a token/wallet",
},
"get_news_sentiment": {
"function": get_news_sentiment,
"free_tier": True,
"pro_cents": 50,
"ent_cents": 40,
"description": "Get news sentiment analysis",
},
"get_wallet_labels": {
"function": get_wallet_labels,
"free_tier": True,
"pro_cents": 20,
"ent_cents": 15,
"description": "Get labels for a wallet address",
},
"get_entity_labels": {
"function": get_entity_labels,
"free_tier": False,
"pro_cents": 50,
"ent_cents": 40,
"description": "Get entity labels from Neo4j graph",
},
}
def list_tools() -> list[dict]:
"""List all available x402 tools."""
return [
{
"tool_id": name,
"description": info["description"],
"free_tier": info["free_tier"],
"pro_cents": info.get("pro_cents"),
"ent_cents": info.get("ent_cents"),
}
for name, info in TOOLS.items()
]
def get_tool(tool_id: str) -> dict | None:
"""Get a tool by ID."""
return TOOLS.get(tool_id)
async def call_tool(tool_id: str, **kwargs) -> dict:
"""Call a tool by ID with arguments."""
tool = TOOLS.get(tool_id)
if not tool:
return {"error": f"Unknown tool: {tool_id}"}
try:
result = await tool["function"](**kwargs)
return {"tool_id": tool_id, "result": result}
except Exception as e:
return {"error": str(e)}

View file

@ -0,0 +1,27 @@
"""x402 label tools - get_wallet_labels, get_entity_labels."""
from typing import Any
async def get_wallet_labels(wallet_address: str) -> dict[str, Any]:
"""Get labels for a wallet."""
return {
"wallet_address": wallet_address,
"labels": [],
}
async def get_entity_labels(entity_id: str) -> dict[str, Any]:
"""Get entity labels from Neo4j graph."""
return {
"entity_id": entity_id,
"labels": [],
}
async def get_entity_by_labels(labels: list[str]) -> dict[str, Any]:
"""Get entity by labels."""
return {
"labels": labels,
"entities": [],
}

View file

@ -0,0 +1,37 @@
"""x402 market tools - get_market_overview, get_trending."""
from typing import Any
async def get_market_overview(chain: str = "solana") -> dict[str, Any]:
"""Get market overview for a chain."""
return {
"chain": chain,
"market_status": "active",
"top_tokens": [],
"volume_24h": 0,
"trending": [],
}
async def get_trending(chain: str = "solana") -> dict[str, Any]:
"""Get trending tokens."""
return {
"chain": chain,
"trending_tokens": [],
"time_window": "24h",
}
async def market_overview(chain: str = "solana") -> dict[str, Any]:
"""Alias for get_market_overview."""
return get_market_overview(chain)
async def token_deep_dive(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Deep dive on a token."""
return {
"token_address": token_address,
"chain": chain,
"deep_dive": "pending",
}

View file

@ -0,0 +1,31 @@
"""x402 news tools - get_news, get_news_sentiment."""
from typing import Any
async def get_news(token: str, chain: str = "solana") -> dict[str, Any]:
"""Get news mentions for a token."""
return {
"token": token,
"chain": chain,
"news": [],
"count": 0,
}
async def get_news_sentiment(token: str, chain: str = "solana") -> dict[str, Any]:
"""Get news sentiment analysis."""
return {
"token": token,
"chain": chain,
"sentiment": "neutral",
"articles": 0,
}
async def social_signal(query: str) -> dict[str, Any]:
"""Get social signal."""
return {
"query": query,
"signal": "pending",
}

View file

@ -0,0 +1,220 @@
"""x402 report tools - generate_report, get_report."""
from typing import Any
async def generate_report(subject_type: str, subject_id: str, model: str = "deepseek-v3") -> dict[str, Any]:
"""Generate a research report."""
return {
"report_id": "pending",
"subject_type": subject_type,
"subject_id": subject_id,
"sections": {},
"risk_score": 50,
"markdown": "# Pending Report\n\n[Report generation pending]",
}
async def get_report(report_id: str) -> dict[str, Any]:
"""Get a previously generated report."""
return {
"report_id": report_id,
"status": "pending",
"result": {},
}
async def social_sentiment(token: str, chain: str = "solana") -> dict[str, Any]:
"""Get social sentiment analysis."""
return {
"token": token,
"chain": chain,
"sentiment": "neutral",
"mentions": 0,
}
async def cluster_detection(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
"""Run cluster detection."""
return {
"wallet_address": wallet_address,
"chain": chain,
"clusters": [],
}
async def insider_tracker(creator: str, chain: str = "solana") -> dict[str, Any]:
"""Track insider activity."""
return {
"creator": creator,
"chain": chain,
"insider_activity": [],
}
async def twitter_profile(query: str) -> dict[str, Any]:
"""Get Twitter profile."""
return {
"query": query,
"profile": {},
}
async def twitter_timeline(query: str) -> dict[str, Any]:
"""Get Twitter timeline."""
return {
"query": query,
"tweets": [],
}
async def twitter_search(query: str) -> dict[str, Any]:
"""Search Twitter."""
return {
"query": query,
"results": [],
}
async def launch_radar(chain: str = "solana", window_minutes: int = 5) -> dict[str, Any]:
"""Get launch radar."""
return {
"chain": chain,
"window_minutes": window_minutes,
"launches": [],
}
async def launch_intel(chain: str = "solana", hours: int = 24) -> dict[str, Any]:
"""Get launch intelligence."""
return {
"chain": chain,
"hours": hours,
"launches": [],
}
async def token_pulse(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Get token pulse."""
return {
"token_address": token_address,
"chain": chain,
"pulse": "pending",
}
async def anomaly_detector(chain: str = "solana") -> dict[str, Any]:
"""Run anomaly detection."""
return {
"chain": chain,
"anomalies": [],
}
async def whale_decoder(address: str, chain: str = "solana") -> dict[str, Any]:
"""Decode whale activity."""
return {
"address": address,
"chain": chain,
"whale_activity": [],
}
async def chain_health(chain: str = "solana") -> dict[str, Any]:
"""Check chain health."""
return {
"chain": chain,
"health": "pending",
}
async def portfolio_tracker(addresses: list[str], chain: str = "solana") -> dict[str, Any]:
"""Track portfolio."""
return {
"addresses": addresses,
"chain": chain,
"portfolio": [],
}
async def copy_trade_finder(chain: str = "solana") -> dict[str, Any]:
"""Find copy trade opportunities."""
return {
"chain": chain,
"opportunities": [],
}
async def risk_monitor(address: str, chain: str = "solana") -> dict[str, Any]:
"""Monitor risk."""
return {
"address": address,
"chain": chain,
"risk_level": "pending",
}
async def defi_yield_scanner(chain: str = "solana") -> dict[str, Any]:
"""Scan DeFi yield opportunities."""
return {
"chain": chain,
"opportunities": [],
}
async def nft_wash_detector(collection: str) -> dict[str, Any]:
"""Detect NFT wash trading."""
return {
"collection": collection,
"wash_trading": False,
}
async def bridge_security() -> dict[str, Any]:
"""Check bridge security."""
return {
"bridge_security": "pending",
}
async def gas_forecast(chain: str = "solana") -> dict[str, Any]:
"""Forecast gas prices."""
return {
"chain": chain,
"gas_forecast": "pending",
}
async def sniper_alert(chain: str = "solana", hours: int = 24) -> dict[str, Any]:
"""Get sniper alerts."""
return {
"chain": chain,
"hours": hours,
"sniper_alerts": [],
}
async def liquidity_flow(address: str, chain: str = "solana") -> dict[str, Any]:
"""Track liquidity flow."""
return {
"address": address,
"chain": chain,
"liquidity_flow": [],
}
async def rug_pull_predictor(address: str, chain: str = "solana") -> dict[str, Any]:
"""Predict rug pull risk."""
return {
"address": address,
"chain": chain,
"risk_score": 50,
}
async def airdrop_finder(chain: str = "solana") -> dict[str, Any]:
"""Find airdrop opportunities."""
return {
"chain": chain,
"airdrops": [],
}

View file

@ -0,0 +1,50 @@
"""x402 scanner tools - run_scan, get_scan_result."""
from typing import Any
async def run_scan(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Run full SENTINEL scan."""
return {
"token_address": token_address,
"chain": chain,
"scan_status": "pending",
"modules_run": [],
"safety_score": 50,
}
async def get_scan_result(scan_id: str) -> dict[str, Any]:
"""Get scan result by ID."""
return {
"scan_id": scan_id,
"status": "pending",
"result": {},
}
async def rug_shield(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Run rug shield scan."""
return {
"token_address": token_address,
"chain": chain,
"rug_shield": "pending",
}
async def honeypot_check(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Check honeypot status."""
return {
"token_address": token_address,
"chain": chain,
"is_honeypot": False,
}
async def token_forensics(token_address: str, chain: str = "solana") -> dict[str, Any]:
"""Run token forensics."""
return {
"token_address": token_address,
"chain": chain,
"forensics": "pending",
}

View file

@ -0,0 +1,37 @@
"""x402 token tools - get_token_risk, get_token_info, deep_contract_audit."""
from typing import Any
async def get_token_risk(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
"""Get risk analysis for a token."""
return {
"token_address": token_address,
"chain": chain,
"risk_score": 50, # Placeholder
"risk_tier": "UNKNOWN",
"risk_factors": [],
"notes": "Token risk analysis - full implementation in x402_tools.py",
}
async def get_token_info(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
"""Get basic token info."""
return {
"token_address": token_address,
"chain": chain,
"symbol": "UNKNOWN",
"name": "Unknown Token",
"decimals": 0,
"total_supply": 0,
}
async def deep_contract_audit(token_address: str, chain: str = "ethereum") -> dict[str, Any]:
"""Deep contract audit."""
return {
"token_address": token_address,
"chain": chain,
"audit_result": "PENDING",
"findings": [],
}

View file

@ -0,0 +1,34 @@
"""x402 wallet tools - get_wallet_analysis, get_wallet_history, wallet_profiler."""
from typing import Any
async def get_wallet_analysis(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
"""Get wallet behavior analysis."""
return {
"wallet_address": wallet_address,
"chain": chain,
"analysis": "pending",
"personas": [],
"risk_score": 50,
}
async def get_wallet_history(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
"""Get wallet transaction history."""
return {
"wallet_address": wallet_address,
"chain": chain,
"transactions": [],
"total_count": 0,
}
async def wallet_profiler(wallet_address: str, chain: str = "solana") -> dict[str, Any]:
"""Profile a wallet."""
return {
"wallet_address": wallet_address,
"chain": chain,
"profile": "pending",
"metrics": {},
}