#!/usr/bin/env python3 """x402client.py — reference client for paying x402-gated services (basebalance.cloud /rpc). Pure stdlib. Use from ANY agent that can sign a USDC transfer on Base: 1. POST /rpc -> free tier serves (10 req/min/IP), check x-quota-* headers. 2. On HTTP 402, parse the challenge (USDC on Base 8453, amount, payTo, token). 3. Build + sign a standard ERC20 transfer(USDC, payTo, amount) with YOUR wallet, broadcast it on Base, wait for status 0x1. 4. Retry the request with header x-paywall-tx: <0x..txhash>. 5. GET /quota to confirm: tier: "paid", remaining 10,000. Run: python3 x402client.py [your-address] [rpc-timeout] """ import json, sys, time, urllib.request RPC_URL = "http://localhost:8000/rpc" # override with argv[1] QUOTA_URL = RPC_URL.replace("/rpc", "/quota") BASE_RPCS = ["https://mainnet.base.org", "https://base-rpc.publicnode.com", "https://base-rpc.com", "https://1rpc.io/base", "https://base.llamarpc.com"] UA = {"User-Agent": "Mozilla/5.0 (nexus-x402client)", "Content-Type": "application/json"} def rpc_call(rpc, method, params): req = urllib.request.Request(rpc, data=json.dumps( {"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode(), headers=UA) with urllib.request.urlopen(req, timeout=12) as r: return json.load(r).get("result") def wait_receipt(txhash, chain_rpcs=BASE_RPCS, timeout_s=120): """Wait for a Base tx to confirm; return receipt or None.""" deadline = time.time() + timeout_s while time.time() < deadline: for rpc in chain_rpcs: try: rcpt = rpc_call(rpc, "eth_getTransactionReceipt", [txhash]) if rcpt: return rcpt except Exception: continue time.sleep(4) return None def free_call(url): body = json.dumps({"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}).encode() req = urllib.request.Request(url, data=body, headers=UA) try: with urllib.request.urlopen(req, timeout=15) as r: print(" free tier: HTTP %s" % r.status) for h in ("x-quota-tier", "x-quota-used", "x-quota-remaining", "x-quota-expiry", "x-rpc-upstream", "x-rpc-cache"): if r.headers.get(h): print(" %s: %s" % (h, r.headers.get(h))) return json.load(r) except urllib.error.HTTPError as e: if e.code == 402: body = json.load(e) ch = body.get("challenge", {}) print(" HTTP 402 — payment required") print(" scheme=%s network=%s amount=%s payTo=%s token=%s" % ( ch.get("scheme"), ch.get("network"), ch.get("amount"), ch.get("payTo"), (ch.get("token") or "")[:10] + "...")) return ch raise def dance(gateway_url): print("step 1 — free request:") ch = free_call(gateway_url) if not isinstance(ch, dict) or "payTo" not in ch: print(" (free tier sufficed; no payment needed)") return True print() print("step 2 — pay (you sign + broadcast this on Base 8453):") print(" USDC token : 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") print(" transfer to: %s" % ch["payTo"]) print(" amount : %s USDC (>= %s)" % (ch.get("amount"), ch.get("amount"))) print(" then: python3 x402client.py CLAIM %s " % gateway_url) return False def claim(gateway_url, txhash): print("step 3 — wait for confirmation...") rcpt = wait_receipt(txhash) if not rcpt or rcpt.get("status") != "0x1": print(" not confirmed (yet) — check the tx on Base explorer") return False print(" confirmed, status 0x1") body = json.dumps({"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}).encode() req = urllib.request.Request(gateway_url, data=body, headers=dict(UA, **{"x-paywall-tx": txhash})) with urllib.request.urlopen(req, timeout=15) as r: print(" quota tier after claim: %s" % r.headers.get("x-quota-tier")) print(" remaining: %s" % r.headers.get("x-quota-remaining")) print("done — quota granted (10,000 req or 24h). GET %s to confirm." % QUOTA_URL) if __name__ == "__main__": url = sys.argv[1] if len(sys.argv) > 1 else RPC_URL if len(sys.argv) > 2 and sys.argv[2].lower() == "claim": claim(url, sys.argv[3]) else: print("x402 client demo — gateway: %s" % url) dance(url)