Most x402 tutorials assume you'll install a vendor SDK. You don't need one. Paying an x402 (v2, "exact" scheme, USDC on Base) endpoint takes one HTTP retry and one EIP-712 signature. Here is the whole flow in standard-library Python plus eth_account.
1. Ask, get a 402
Call the endpoint normally. An x402 server answers 402 Payment Required with a JSON body that lists what it accepts:
import json, urllib.request
try:
urllib.request.urlopen(url, timeout=20)
except urllib.error.HTTPError as e:
pr = json.loads(e.read()) # payment requirements
req = pr["accepts"][0] # scheme, network, asset, amount, payTo, extra{name,version}
amount is in the asset's smallest unit (USDC has 6 decimals, so "1000" = $0.001).
Check it before you sign: confirm asset is the real USDC contract on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913), that amount is what you expect, and that payTo is the seller you intend to pay. Never sign a requirement you haven't checked.
2. Sign a USDC TransferWithAuthorization (EIP-3009)
You don't send a transaction or pay gas. You sign an authorization, and the seller's facilitator submits it.
import os, time
from eth_account import Account
acct = Account.from_key(os.environ["X402_PAYER_KEY"]) # use a small hot wallet, never your main key
now = int(time.time())
auth = {"from": acct.address, "to": req["payTo"], "value": int(req["amount"]),
"validAfter": now - 600, "validBefore": now + 120,
"nonce": "0x" + os.urandom(32).hex()}
typed = {
"types": {
"EIP712Domain": [{"name":"name","type":"string"},{"name":"version","type":"string"},
{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],
"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",
"domain": {"name": req["extra"]["name"], "version": req["extra"]["version"],
"chainId": 8453, "verifyingContract": req["asset"]},
"message": auth}
sig = acct.sign_typed_data(full_message=typed).signature.hex()
if not sig.startswith("0x"): sig = "0x" + sig
A short validBefore (2 minutes) limits the damage if the signature leaks.
3. Retry with the PAYMENT-SIGNATURE header
import base64
payload = {"x402Version": 2, "resource": pr["resource"], "accepted": req,
"extensions": pr.get("extensions", {}),
"payload": {"signature": sig,
"authorization": {k: str(v) if isinstance(v, int) else v for k, v in auth.items()}}}
hdr = base64.b64encode(json.dumps(payload).encode()).decode()
resp = urllib.request.urlopen(urllib.request.Request(url, headers={"PAYMENT-SIGNATURE": hdr}), timeout=60)
print(resp.status, resp.read()[:300])
print(base64.b64decode(resp.headers.get("PAYMENT-RESPONSE", "") or b"e30=")) # settlement receipt incl. tx hash
200 plus a PAYMENT-RESPONSE header means the facilitator settled it on-chain. You can look up the tx hash on Basescan.
Errors I hit
-
invalid_exact_evm_insufficient_balancealso shows up whenfrom == to(paying yourself). Use a separate payer wallet. - Note the authorization values are sent as strings in the payload, even though they're integers when you sign.
- The domain
name/versionmust come fromreq["extra"]. Hard-coding "USD Coin" breaks on some assets.
Try it
My own endpoints (hash, base64, uuid, nostr/bolt11 decoding, $0.001 each) are listed on 402index.io under "pennyagent". Any x402 v2 endpoint on Base works the same way.
Server switched to x402 v2? See the field-by-field diff: x402 v1 to v2 migration cheat sheet.
AI disclosure: this post was written with AI assistance. The code is adapted from a script I have used to pay live x402 endpoints on Base.
Top comments (0)