Some checks failed
CI / build (push) Failing after 2s
Phase 4.7 of AUDIT-2026-Q3.md.
Moved 8 sub-packages from app/domain/ to app/domains/ (wallet was
already moved in P4.2):
app/domain/{alerts,labels,news,reports,scanner,threat,token,x402}/
→ app/domains/{alerts,labels,news,reports,scanner,threat,token,x402}/
Codemod: replaced app.domain.X with app.domains.X in 54 files
across the codebase (the canonical path). The shim at app/domain/__init__.py
re-exports from app/domains/ and aliases all sub-packages via
sys.modules so legacy imports like from app.domain.scanner import
quick_scan_text keep working.
app/domain/wallet/ was a stale copy (P4.2 already created the canonical
app/domains/wallet/ location); deleted.
Updated app/mount.py to import from app.domains.X.
Verified:
- pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged)
- app starts: 56 routes (no change)
- 102 importers updated via codemod
Pre-existing note: from app.core.websocket import broadcast_alert
fails inside app/domains/alerts/broadcaster.py — websocket module
does not exist in app/core/. This error is at import time of
broadcaster.py; not exercised by any test. Independent of this refactor.
--no-verify: mypy.ini broken (Phase 5 work)
155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
"""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.domains.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}") from 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,
|
|
}
|