Spaces:
Running
Running
Download scripts/x402_api/main.py from eltociear/contract-guard: direct link, hf CLI and curl.
- Browser
- Download file 27.8 kB
-
https://huggingface.co/spaces/eltociear/contract-guard/resolve/main/scripts/x402_api/main.py
- Command line
-
hf download hf://spaces/eltociear/contract-guard/scripts/x402_api/main.py
-
curl -L -o main.py https://huggingface.co/spaces/eltociear/contract-guard/resolve/main/scripts/x402_api/main.py
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" | |
| 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, | |
| }, | |
| } | |
| 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. | |
| async def well_known_x402(): | |
| return { | |
| "version": 1, | |
| "resources": [PUBLIC_BASE + "/check"], | |
| "instructions": _INSTRUCTIONS, | |
| "payTo": [WALLET], | |
| "network": BASE_MAINNET, | |
| } | |
| 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} | |
| 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}, | |
| } | |
| 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"}, | |
| ], | |
| }], | |
| } | |
| 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", | |
| ) | |
| 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. | |
| 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", | |
| }, | |
| } | |
| 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}], | |
| } | |
| 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", | |
| } | |
| 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") | |
| async def m2m_openapi(): | |
| """Alias: something in the wild probes this name for a machine-readable spec.""" | |
| return app.openapi() | |
| 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 | |
| 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) | |