"""Stack Facts: pay for one call with x402 from Python, standard library only (Python 3.9+). python3 python_x402.py [url] [json-body] Default url: https://g5joilnmeep4ntz3nwezxt5dmu.srv.us/v1/eol/nodejs (GET); a json-body makes it a POST. Env: MAX_ATOMIC = the most this script will pay for one call, in atomic USDC (6 decimals; default 10000 = $0.01). The flow (x402 v2, scheme "exact"): call the route -> 402 with the offer in the PAYMENT-REQUIRED header (base64 JSON) -> sign an EIP-3009 TransferWithAuthorization for exactly that offer -> call again with PAYMENT-SIGNATURE (base64 JSON) -> 200 with the answer and PAYMENT-RESPONSE (base64 JSON: the settlement transaction). The default signer is a placeholder that cannot pay. To pay, replace mock_sign with an EIP-712 signer for the typed data built below (for example eth_account's Account.sign_typed_data with a key holding USDC), and MOCK_ADDRESS with that key's address. """ import base64 import json import os import secrets import sys import time import urllib.error import urllib.request DEFAULT_URL = "https://g5joilnmeep4ntz3nwezxt5dmu.srv.us/v1/eol/nodejs" CHAIN_IDS = {"eip155:8453": 8453} # Base MOCK_ADDRESS = "0x5555555555555555555555555555555555555555" def encode(value): """Standard base64 of UTF-8 JSON (the header encoding).""" return base64.b64encode(json.dumps(value, separators=(",", ":")).encode("utf-8")).decode("ascii") def decode(b64): return json.loads(base64.b64decode(b64.strip()).decode("utf-8")) def mock_sign(typed_data): """Placeholder signer: a dummy 65-byte signature. Replace it to pay.""" return "0x" + "cd" * 65 def transfer_authorization(accepted, sender, now=None): """The EIP-712 typed data a real wallet signs for one "exact" offer (EIP-3009 on USDC).""" chain_id = CHAIN_IDS.get(accepted["network"]) if chain_id is None: raise ValueError("unknown network %s" % accepted["network"]) now = int(time.time()) if now is None else now message = { "from": sender, "to": accepted["payTo"], "value": accepted["amount"], "validAfter": "0", "validBefore": str(now + accepted["maxTimeoutSeconds"]), "nonce": "0x" + secrets.token_hex(32), } return { "domain": {"name": accepted["extra"]["name"], "version": accepted["extra"]["version"], "chainId": chain_id, "verifyingContract": accepted["asset"]}, "types": {"TransferWithAuthorization": [ {"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "value", "type": "uint256"}, {"name": "validAfter", "type": "uint256"}, {"name": "validBefore", "type": "uint256"}, {"name": "nonce", "type": "bytes32"}, ]}, "primaryType": "TransferWithAuthorization", "message": message, } def call(url, body=None, headers=None): """-> (status, headers, body bytes). A 4xx/5xx is an answer here, not an exception.""" headers = dict(headers or {}) data = None if body is not None: data = body.encode("utf-8") headers["content-type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method="POST" if data is not None else "GET") try: with urllib.request.urlopen(req, timeout=30) as res: return res.status, res.headers, res.read() except urllib.error.HTTPError as err: return err.code, err.headers, err.read() def paying_call(url, body=None, max_atomic=10000, sign=mock_sign, sender=MOCK_ADDRESS): """Calls url, pays one 402 offer of at most max_atomic. -> (status, body bytes, accepted, settlement).""" status, headers, raw = call(url, body) if status != 402: return status, raw, None, None offer = decode(headers["PAYMENT-REQUIRED"]) accepted = next((a for a in offer["accepts"] if a["scheme"] == "exact" and a["network"] in CHAIN_IDS), None) if accepted is None: raise RuntimeError("no offer this client can pay (needs scheme exact on Base)") if int(accepted["amount"]) > int(max_atomic): raise RuntimeError("price %s atomic USDC is over the limit of %s; not paying" % (accepted["amount"], max_atomic)) typed = transfer_authorization(accepted, sender) payment = { "x402Version": offer["x402Version"], "resource": offer["resource"], "accepted": accepted, "payload": {"signature": sign(typed), "authorization": typed["message"]}, } status, headers, raw = call(url, body, {"PAYMENT-SIGNATURE": encode(payment)}) settled = headers.get("PAYMENT-RESPONSE") return status, raw, accepted, decode(settled) if settled else None def main(argv): url = argv[1] if len(argv) > 1 else DEFAULT_URL body = argv[2] if len(argv) > 2 else None try: status, raw, accepted, paid = paying_call(url, body, int(os.environ.get("MAX_ATOMIC", "10000"))) except (RuntimeError, ValueError, KeyError, urllib.error.URLError) as err: print("python_x402: %s" % err, file=sys.stderr) return 2 try: answer = json.loads(raw.decode("utf-8")) except ValueError: answer = raw.decode("utf-8", "replace") price = None if accepted: price = {k: accepted[k] for k in ("amount", "asset", "network", "payTo")} print(json.dumps({"status": status, "price": price, "paid": paid, "body": answer}, indent=2)) return 0 if 200 <= status < 300 else 1 if __name__ == "__main__": sys.exit(main(sys.argv))