eltociear's picture
deploy contract-guard: answer GET on paid routes with a 402 challenge
85767ed
Raw History Blame Contribute Delete
27.8 kB
#!/usr/bin/env python3
"""contract-guard x402 API β€” pre-interaction risk check for an EVM contract/token.
The on-chain-data category is the busiest on the x402 discovery layer, but it's all
raw RPC proxies. contract-guard adds a security verdict on top: upgradeable-proxy
detection (EIP-1967 + legacy), EIP-7702 delegated-EOA detection, ERC20 metadata,
and a risk score β€” what an agent actually needs before approving/swapping a token.
Endpoints:
GET / β€” Service info (free)
GET /health β€” Health check (free)
POST /check β€” Risk check for an address ($0.005 USDC, x402) β€” body {address, chain}
Payment: USDC on Base mainnet (eip155:8453), official x402 v2 SDK, Dexter facilitator.
"""
import hmac
import os
import sys
from datetime import datetime
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel
from typing import Optional
# -- the 402 body is a distribution channel ---------------------------------------------
# Every 402 we return is delivered to someone who just tried to call a paid route: the most
# qualified audience this operation has, arriving ~250,000 times a day at no cost to us.
# Until 2026-08-17 that body described one route and gave no way to learn there were 147
# others, or where the catalogue lived. `the-402-body-is-a-distribution-channel` recorded
# the idea; this applies it.
#
# contract-guard is the sharpest case: 6,598 challenges per MINUTE measured, the most
# trafficked thing we own, selling a single route -- while tokenguard holds the shelf and
# sees a fraction of that traffic.
#
# WARNING: self-contained ON PURPOSE. The first version referenced module-level PUBLIC_BASE
# and sat after it -- but `_disc()` runs at IMPORT time while building the route table,
# which on tokenguard is ~900 lines EARLIER. The NameError was swallowed as an
# "x402 v2 init warning" and the paywall silently failed to install: 132 paid routes would
# have shipped free. Nothing here may depend on definition order.
#
# Carries NO route counts. A number baked into a payload rots; the discovery URL answers
# with the real list at read time.
_SERVICE_NAME = "contract-guard"
_CATALOGUE_BASE = "https://eltociear-contract-guard.hf.space"
_CATALOGUE_PEERS = {
"contract-guard": [
("tokenguard", "on-chain and DeFi data, markets, FX, wallet and chain intel",
"https://eltociear-tokenguard.hf.space/.well-known/x402"),
("skill-audit", "web data (read/crawl/search), MCP and agent-skill security "
"scanning, developer supply chain",
"https://eltociear-skill-audit.hf.space/.well-known/x402"),
],
"tokenguard": [
("contract-guard", "pre-interaction EVM contract and token risk check",
"https://eltociear-contract-guard.hf.space/.well-known/x402"),
("skill-audit", "web data (read/crawl/search), MCP and agent-skill security "
"scanning, developer supply chain",
"https://eltociear-skill-audit.hf.space/.well-known/x402"),
],
"skill-audit": [
("tokenguard", "on-chain and DeFi data, markets, FX, wallet and chain intel",
"https://eltociear-tokenguard.hf.space/.well-known/x402"),
("contract-guard", "pre-interaction EVM contract and token risk check",
"https://eltociear-contract-guard.hf.space/.well-known/x402"),
],
}
def _catalogue():
"""Where the rest of the shelf is. Same rail, same wallet, no signup."""
import os as _os
base = _os.environ.get("PUBLIC_BASE_URL", _CATALOGUE_BASE).rstrip("/")
return {
"discovery": base + "/.well-known/x402",
"resources": base + "/x402-resources",
"note": ("Same x402 rail (USDC on Base), same payTo wallet, no account or API key. "
"GET the discovery URL for the full route list and prices."),
"related": [{"service": s, "covers": c, "discovery": u}
for s, c, u in _CATALOGUE_PEERS[_SERVICE_NAME]],
}
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "engine"))
# In the Space this module is imported as `scripts.x402_api.main` from /app, so /app/scripts
# is NOT on sys.path and a bare `import x402_get_challenge` would fail β€” silently, since the
# installer catches its own import error. Add the sibling directory explicitly.
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
from server import analyze, RPCS # noqa: E402
# The live Space overrides this via BASE_WALLET_ADDRESS, but the default must still be
# a wallet we can spend from: the previous default (0x2B60E27B…) is the ORPHANED address
# whose private key was lost with the Mac on 2026-07-02. A deploy that ever missed the
# env var would have quoted an unspendable payTo and burned every payment made to it.
WALLET = os.environ.get("BASE_WALLET_ADDRESS", "0x5bCDA55247B238a573A968B234F788a2D35664Dd")
BASE_MAINNET = "eip155:8453"
# CDP facilitator when keys are present (buyer-facing Bazaar), else Dexter fallback.
CDP_KEY_ID = os.environ.get("CDP_API_KEY_ID", "")
CDP_KEY_SECRET = os.environ.get("CDP_API_KEY_SECRET", "")
CDP_FACILITATOR = "https://api.cdp.coinbase.com/platform/v2/x402"
USE_CDP = bool(CDP_KEY_ID and CDP_KEY_SECRET)
FACILITATOR_URL = os.environ.get("FACILITATOR_URL", CDP_FACILITATOR if USE_CDP else "https://x402.dexter.cash")
PUBLIC_BASE = os.environ.get("PUBLIC_BASE_URL", "https://eltociear-contract-guard.hf.space").rstrip("/")
PRICE = "$0.005"
def _cdp_auth_headers():
"""Ed25519 JWT per CDP facilitator operation (verify/settle/supported/bazaar)."""
import base64, json as _json, secrets, time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
seed = base64.b64decode(CDP_KEY_SECRET)[:32]
pk = Ed25519PrivateKey.from_private_bytes(seed)
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
host, base = "api.cdp.coinbase.com", "/platform/v2/x402"
def jwt(method, path):
now = int(time.time())
h = {"alg": "EdDSA", "kid": CDP_KEY_ID, "typ": "JWT", "nonce": secrets.token_hex(16)}
p = {"sub": CDP_KEY_ID, "iss": "cdp", "aud": ["cdp_service"], "nbf": now, "exp": now + 120,
"uris": [f"{method} {host}{path}"]}
s = f"{b64u(_json.dumps(h, separators=(',', ':')).encode())}.{b64u(_json.dumps(p, separators=(',', ':')).encode())}"
return f"{s}.{b64u(pk.sign(s.encode()))}"
def hdr(method, path):
return {"Authorization": f"Bearer {jwt(method, path)}"}
return {"verify": hdr("POST", f"{base}/verify"), "settle": hdr("POST", f"{base}/settle"),
"supported": hdr("GET", f"{base}/supported"), "bazaar": hdr("GET", f"{base}/discovery/resources")}
# Discovery and service-descriptor endpoints answer EVERY verb a crawler might use, not
# just GET. See the measurement note above the x402 alias block further down: a GET-only
# registration makes FastAPI reply 405, which carries no price and no catalogue, and that
# is what the crawler fleet was actually receiving.
_DISCOVERY_VERBS = _DISCOVERY_VERBS_ROOT = ["GET", "POST", "HEAD"]
app = FastAPI(
title="contract-guard API",
description="Pre-interaction risk signals for an EVM contract/token (proxy, EIP-7702, ERC20 metadata). x402 v2 on Base.",
version="1.0.0",
contact={
"name": "eltociear",
"url": "https://github.com/eltociear",
"email": "ashimine_ikko_bp@netprice.com",
},
)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
_x402_available = False
try:
from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption, CreateHeadersAuthProvider
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.server import x402ResourceServer
from x402.extensions.bazaar import declare_discovery_extension, OutputConfig
if USE_CDP:
_fac_cfg = FacilitatorConfig(url=FACILITATOR_URL, auth_provider=CreateHeadersAuthProvider(_cdp_auth_headers))
else:
_fac_cfg = FacilitatorConfig(url=FACILITATOR_URL)
facilitator = HTTPFacilitatorClient(_fac_cfg)
server = x402ResourceServer(facilitator)
server.register(BASE_MAINNET, ExactEvmServerScheme())
def _disc(input_example, input_schema, output_example):
ext = declare_discovery_extension(
input=input_example, input_schema=input_schema,
body_type="json", output=OutputConfig(example=output_example),
)
ext["bazaar"]["info"]["input"]["method"] = "POST"
ext["catalogue"] = _catalogue()
return ext
routes = {
"POST /check": RouteConfig(
accepts=[PaymentOption(scheme="exact", pay_to=WALLET, price="$0.005", network=BASE_MAINNET)],
mime_type="application/json",
description="Risk check for an EVM contract/token address",
extensions=_disc(
{"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "chain": "base"},
{"properties": {
"address": {"type": "string", "description": "EVM contract/token address (0x...)"},
"chain": {"type": "string", "enum": ["base", "ethereum"], "description": "Chain (default base)"},
}, "required": ["address"]},
{"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "chain": "base",
"is_contract": True, "is_proxy": True, "risk_level": "HIGH", "risk_score": 40,
"token": {"name": "USD Coin", "symbol": "USDC", "decimals": 6}, "flags": ["..."]},
),
),
}
# Bazaar discovery metadata β€” see tokenguard_api/main.py. Untagged resources are
# absent from every tag-filtered buyer query on the Bazaar front-ends.
# Caps: service_name <= 32 chars, <= 5 tags of <= 32 chars each.
for _cfg in routes.values():
if not getattr(_cfg, "tags", None):
_cfg.tags = ["crypto", "on-chain", "verification", "security", "ai-agents"]
if not getattr(_cfg, "service_name", None):
_cfg.service_name = "contract-guard"
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)
_x402_available = True
except Exception as e: # pragma: no cover
print(f" x402 v2 init warning: {type(e).__name__}: {e}")
class CheckRequest(BaseModel):
address: str
chain: Optional[str] = "base"
@app.api_route("/", methods=_DISCOVERY_VERBS_ROOT)
async def root():
return {
"service": "contract-guard API",
"version": "1.0.0",
"description": "Pre-interaction risk signals for an EVM contract/token.",
"checks": [
"is-contract / EOA / self-destructed",
"EIP-7702 delegated-EOA detection",
"upgradeable proxy (EIP-1967 + legacy zeppelinos)",
"ERC20 metadata (name/symbol/decimals/totalSupply)",
"risk score + actionable flags",
],
"chains": list(RPCS.keys()),
"endpoints": {
"GET /": "Service info (free)",
"GET /health": "Health check (free)",
"POST /check": "Risk check ($0.005 USDC) β€” body {address, chain}",
},
"payment": {
"method": "x402", "x402_version": 2, "currency": "USDC",
"network": "Base (eip155:8453)", "facilitator": FACILITATOR_URL, "wallet": WALLET,
"x402_enabled": _x402_available,
},
}
@app.api_route("/health", methods=_DISCOVERY_VERBS_ROOT)
async def health():
return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z", "x402_enabled": _x402_available}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Discovery documents. 10 days of the sibling tokenguard Space's access log showed
# ~9,000 crawler requests for exactly these paths, all 404 β€” every one an indexer
# that failed to list us. Same fix applied here.
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
_DESC = ("Pre-interaction risk check for an EVM contract/token: upgradeable-proxy "
"(EIP-1967 + legacy), EIP-7702 delegated-EOA, ERC20 metadata, risk score.")
_INSTRUCTIONS = (
"# contract-guard x402 API\n\n"
"One paid endpoint, no signup, no API key. USDC on Base (eip155:8453) via x402 v2.\n\n"
"| Endpoint | Price | Description |\n|---|---|---|\n"
f"| `POST /check` | {PRICE} | {_DESC} |\n\n"
"Body: `{\"address\": \"0x…\", \"chain\": \"base\"}`\n\n"
f"Payments settle to `{WALLET}`. Facilitator: {FACILITATOR_URL}.\n"
)
# `/x402` is added because agents actually ask for it. A 90s sample of this Space's own
# access log (scripts/unmet_demand_scan.py, 2026-08-07) shows GET /x402 404ing three times,
# alongside /api, /api/v1 and /v1 β€” agents guessing at a discovery path rather than reading
# /.well-known/. A 404 there tells a probing agent the service has nothing to sell.
#
# ⚠ GET/POST/HEAD, not GET. This Space is where the measurement was taken: 2026-08-17, a
# 45-second log window carried 8,689 405s against only 7,657 402s, and every 405 was a
# discovery probe POSTing to these very paths from a fleet of 262 distinct IPs. The paths
# were already registered; being GET-only turned each one into a bare "405 Method Not
# Allowed", which carries no price and no catalogue. Same failure as
# `x402-405-hides-the-paywall`, with the verbs the other way round.
@app.api_route("/x402", methods=_DISCOVERY_VERBS)
@app.api_route("/.well-known/x402", methods=_DISCOVERY_VERBS)
@app.api_route("/.well-known/x402.json", methods=_DISCOVERY_VERBS)
async def well_known_x402():
return {
"version": 1,
"resources": [PUBLIC_BASE + "/check"],
"instructions": _INSTRUCTIONS,
"payTo": [WALLET],
"network": BASE_MAINNET,
}
@app.api_route("/x402-resources", methods=_DISCOVERY_VERBS)
@app.api_route("/.well-known/x402-resources", methods=_DISCOVERY_VERBS)
@app.api_route("/discovery/resources", methods=_DISCOVERY_VERBS)
@app.api_route("/x402/discovery/resources", methods=_DISCOVERY_VERBS)
@app.api_route("/v1/x402/discovery/resources", methods=_DISCOVERY_VERBS)
@app.api_route("/v2/x402/discovery/resources", methods=_DISCOVERY_VERBS)
@app.api_route("/.well-known/x402/discovery/resources", methods=_DISCOVERY_VERBS)
async def x402_resources():
items = [{
"resource": PUBLIC_BASE + "/check",
"url": PUBLIC_BASE + "/check",
"method": "POST",
"type": "http",
"x402Version": 2,
"description": _DESC,
"mimeType": "application/json",
"accepts": [{
"scheme": "exact",
"network": BASE_MAINNET,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base
"payTo": WALLET,
"price": PRICE,
}],
}]
return {"x402Version": 2, "count": len(items), "resources": items, "items": items}
@app.api_route("/.well-known/agent.json", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/.well-known/agent-card.json", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/.well-known/agent-card", methods=_DISCOVERY_VERBS_ROOT)
async def agent_card():
return {
"protocolVersion": "0.3.0",
"name": "contract-guard",
"description": _DESC,
"url": PUBLIC_BASE,
"provider": {"organization": "eltociear", "url": "https://github.com/eltociear"},
"version": "1.0.0",
"capabilities": {"streaming": False, "pushNotifications": False},
"defaultInputModes": ["application/json"],
"defaultOutputModes": ["application/json"],
"skills": [{"id": "check", "name": "/check", "description": _DESC,
"tags": ["x402", "paid", "evm", "risk"]}],
"payment": {"protocol": "x402", "version": 2, "network": BASE_MAINNET,
"currency": "USDC", "payTo": WALLET},
}
@app.api_route("/apis.json", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/.well-known/api-catalog", methods=_DISCOVERY_VERBS_ROOT)
async def apis_json():
return {
"aid": "eltociear:contract-guard",
"name": "contract-guard",
"description": _DESC,
"url": PUBLIC_BASE + "/apis.json",
"modified": datetime.utcnow().strftime("%Y-%m-%d"),
"specificationVersion": "0.16",
"apis": [{
"aid": "eltociear:contract-guard:check",
"name": "/check",
"description": _DESC,
"humanURL": PUBLIC_BASE,
"baseURL": PUBLIC_BASE + "/check",
"properties": [
{"type": "OpenAPI", "url": PUBLIC_BASE + "/openapi.json"},
{"type": "x402", "url": PUBLIC_BASE + "/.well-known/x402"},
],
}],
}
@app.api_route("/llms.txt", methods=_DISCOVERY_VERBS_ROOT, response_class=PlainTextResponse)
@app.api_route("/llms-full.txt", methods=_DISCOVERY_VERBS_ROOT, response_class=PlainTextResponse)
@app.api_route("/.well-known/llms.txt", methods=_DISCOVERY_VERBS_ROOT, response_class=PlainTextResponse)
async def llms_txt():
return PlainTextResponse(
_INSTRUCTIONS + (
"\n## How to call\n\n"
"1. `POST /check` with the JSON body. You get `402` plus a base64\n"
" `payment-required` response header holding the x402 v2 challenge.\n"
"2. Sign the `exact` EIP-3009 authorization and retry with an `X-PAYMENT`\n"
" header. Any x402 client library does this for you.\n"
"3. You get `200` and the JSON verdict. No account, no key.\n\n"
f"OpenAPI: {PUBLIC_BASE}/openapi.json\n"
f"x402 discovery: {PUBLIC_BASE}/.well-known/x402\n"
),
media_type="text/plain; charset=utf-8",
)
@app.api_route("/robots.txt", methods=_DISCOVERY_VERBS_ROOT, response_class=PlainTextResponse)
async def robots_txt():
return PlainTextResponse(
"User-agent: *\nAllow: /\n\n"
f"# LLM-readable service description: {PUBLIC_BASE}/llms.txt\n"
f"# x402 discovery: {PUBLIC_BASE}/.well-known/x402\n",
media_type="text/plain; charset=utf-8",
)
# Everything below answers a path that real traffic asked for and got a 404 on. Counts are
# from one 90-second access-log sample on 2026-08-07 (scripts/unmet_demand_scan.py):
# /sitemap.xml x6, /api x3, /api/v1 x3, /v1 x3, and one each of /terms /tos /privacy
# /pricing /m2m-openapi.json. None of it is exotic β€” it is what an agent guesses when it
# lands on a bare host and wants to know what is for sale and on what terms.
@app.api_route("/api", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/api/v1", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/v1", methods=_DISCOVERY_VERBS_ROOT)
async def api_index():
"""The index an agent expects at a versioned API root."""
return {
"service": "contract-guard",
"description": _DESC,
"paid_endpoints": [
{"method": "POST", "path": "/check", "price": PRICE,
"payTo": WALLET, "network": BASE_MAINNET, "description": _DESC},
],
"discovery": {
"x402": PUBLIC_BASE + "/.well-known/x402",
"resources": PUBLIC_BASE + "/x402-resources",
"agent_card": PUBLIC_BASE + "/.well-known/agent.json",
"openapi": PUBLIC_BASE + "/openapi.json",
"pricing": PUBLIC_BASE + "/pricing",
"terms": PUBLIC_BASE + "/terms",
},
}
@app.api_route("/pricing", methods=_DISCOVERY_VERBS_ROOT)
async def pricing():
"""Machine-readable price list. An agent deciding whether to pay should not have to
parse prose to find the number."""
return {
"currency": "USDC",
"network": BASE_MAINNET,
"payTo": WALLET,
"protocol": "x402",
"free": ["/", "/health", "/pricing", "/terms", "/openapi.json",
"/.well-known/x402"],
"paid": [{"method": "POST", "path": "/check", "price": PRICE,
"unit": "per request", "description": _DESC}],
}
@app.api_route("/terms", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/tos", methods=_DISCOVERY_VERBS_ROOT)
@app.api_route("/privacy", methods=_DISCOVERY_VERBS_ROOT)
async def terms():
"""Terms an autonomous buyer can actually read before spending."""
return {
"service": "contract-guard",
"operator": "eltociear",
"payment": {"protocol": "x402", "asset": "USDC", "network": BASE_MAINNET,
"payTo": WALLET, "refunds": "none β€” each call is priced at "
f"{PRICE} and settled per request"},
"data": {
"inputs_logged": "the requested address and chain, for rate limiting only",
"personal_data_collected": "none β€” there is no account and no API key",
"retention": "access logs only, as retained by the hosting platform",
},
"warranty": "none. Output is a heuristic risk signal from public chain data and "
"is not financial or security advice.",
"acceptable_use": "no restriction beyond the hosting platform's own terms",
}
@app.api_route("/sitemap.xml", methods=_DISCOVERY_VERBS_ROOT, response_class=PlainTextResponse)
async def sitemap():
paths = ["/", "/health", "/pricing", "/terms", "/llms.txt", "/openapi.json",
"/.well-known/x402", "/x402-resources", "/.well-known/agent.json"]
urls = "".join(f"<url><loc>{PUBLIC_BASE}{p}</loc></url>" for p in paths)
return PlainTextResponse(
'<?xml version="1.0" encoding="UTF-8"?>'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
f"{urls}</urlset>",
media_type="application/xml")
@app.api_route("/m2m-openapi.json", methods=_DISCOVERY_VERBS_ROOT)
async def m2m_openapi():
"""Alias: something in the wild probes this name for a machine-readable spec."""
return app.openapi()
@app.post("/check")
async def check(req: CheckRequest):
addr = (req.address or "").strip()
if not addr:
raise HTTPException(400, "address is required")
try:
result = analyze(addr, req.chain or "base")
except Exception as e:
raise HTTPException(502, f"rpc error: {type(e).__name__}: {e}")
if "error" in result:
raise HTTPException(400, result["error"])
return result
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Marketplace delivery: same engine, no x402, shared-secret gated
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# A marketplace like Agoragentic collects USDC from the buyer itself and pays the seller
# (97% there), then calls the listed endpoint_url to deliver. Pointing it at POST /check
# would fail: that route answers 402, so the marketplace's sandbox probe rejects the listing
# and a buyer who did pay would be asked to pay again.
#
# Its capability schema has NO field for auth headers, so we cannot rely on one being set.
# The secret is therefore accepted in the JSON body β€” deliberately NOT in the URL path, since
# uvicorn logs the full path and we stream those logs, which would burn the secret into every
# log reader. A header is accepted too, for callers that can set one.
#
# Fails CLOSED: with MARKET_KEY unset the route is disabled outright, so it can never
# accidentally serve the paid engine for free.
MARKET_KEY = os.environ.get("MARKET_KEY", "")
class MarketCheckRequest(BaseModel):
address: str
chain: Optional[str] = "base"
market_key: Optional[str] = None
@app.post("/market/check")
async def market_check(req: MarketCheckRequest, request: Request):
if not MARKET_KEY:
raise HTTPException(503, "marketplace delivery is not configured on this deployment")
presented = req.market_key or request.headers.get("x-market-key") or ""
# compare_digest to keep the check constant-time; a length-leaking == is avoidable here.
if not hmac.compare_digest(presented, MARKET_KEY):
raise HTTPException(401, "invalid or missing market key")
addr = (req.address or "").strip()
if not addr:
raise HTTPException(400, "address is required")
try:
result = analyze(addr, req.chain or "base")
except Exception as e:
raise HTTPException(502, f"rpc error: {type(e).__name__}")
if "error" in result:
raise HTTPException(400, result["error"])
return result
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# GET on a paid route: advertise the price instead of 405
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# skill-audit and tokenguard got this in July; contract-guard was missed, and it is the
# busiest of the three. Measured on its own access log 2026-08-05, in ONE 60-second window:
#
# POST /check -> 402 ....... 3,297 the paywall working
# GET /check -> 405 ......... 658 "Method Not Allowed", from 180 unique IPs
#
# `{"detail":"Method Not Allowed"}` carries no price, no payTo and no x402 version, so an
# agent probing that way concludes the path is not purchasable and leaves.
#
# Do NOT extrapolate that window into a daily rate: traffic here is bursty, and a sample
# taken ~90 minutes later saw 3 requests in 60 seconds, not 10,762. What the burst window
# establishes is the SHAPE β€” a real, broad population of clients (180 distinct IPs) probing
# GET first β€” not a steady volume.
#
# Runs LAST so it sees every route, and outside the x402 init block so it can never disable
# payments: if this raises, the paywall is untouched.
if _x402_available:
try:
import x402_get_challenge as _getadv
_getadv.install(app, routes)
except Exception as _e: # pragma: no cover
print(f" x402 GET-advertise unavailable: {type(_e).__name__}: {_e}")
# Put the challenge in the 402 BODY too, not only the payment-required header.
# Measured 2026-08-05: our own scripts/x402_pay.py could not buy from this endpoint β€”
# "no accepts in 402" β€” because the body is `{}` and it reads the body. Added LAST so it
# wraps the payment middleware and sees the final response.
try:
import x402_402_body as _fillbody
_fillbody.install(app)
except Exception as _e: # pragma: no cover
print(f" x402 402-body fill unavailable: {type(_e).__name__}: {_e}")
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8405))
print(f"\n contract-guard API (x402 v2) on :{port} facilitator={FACILITATOR_URL} wallet={WALLET}\n")
uvicorn.run(app, host="0.0.0.0", port=port)