#!/usr/bin/env python3 """x402 reference client — pay once, fetch premium data. Served by the gateway itself. Any AI agent can copy-run this to consume the paid endpoint. pip install web3 eth-account requests PRIVATE_KEY=0x... python client.py The wallet must hold >= the quoted USDC on Base + a little ETH for gas. """ import os, time, requests from web3 import Web3 from eth_account import Account from eth_account.messages import encode_defunct BASE = "https://x402.obolpay.xyz" ENDPOINT = BASE + "/api/v1/protected-data" RPC = "https://mainnet.base.org" UA = {"User-Agent": "x402-client/1.0"} # NOTE: send a User-Agent (Cloudflare blocks empty/raw-urllib) def main(): pk = os.environ["PRIVATE_KEY"] acct = Account.from_key(pk) w3 = Web3(Web3.HTTPProvider(RPC)) # 1) Read the free preview + payment challenge (no spend yet) r = requests.get(ENDPOINT, headers=UA, timeout=30) assert r.status_code == 402, r.status_code ch = r.json()["payment"] print("PREVIEW:", ch.get("preview")) # <- evaluate before paying token = Web3.to_checksum_address(ch["token_contract"]) to = Web3.to_checksum_address(ch["recipient"]) amount = int(round(float(ch["amount"]) * 10 ** 6)) # USDC has 6 decimals domain, invoice = ch["signature_scheme"]["domain"], ch["invoice_id"] # 2) Pay: ERC-20 USDC transfer to the merchant erc20 = w3.eth.contract(address=token, abi=[{"name": "transfer", "type": "function", "stateMutability": "nonpayable", "inputs": [{"name": "to", "type": "address"}, {"name": "value", "type": "uint256"}], "outputs": [{"type": "bool"}]}]) tx = erc20.functions.transfer(to, amount).build_transaction({ "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address), "chainId": ch["chain_id"], "gas": 120000, "maxFeePerGas": w3.eth.gas_price * 2, "maxPriorityFeePerGas": w3.to_wei(0.001, "gwei")}) signed = acct.sign_transaction(tx) raw = getattr(signed, "raw_transaction", None) or signed.rawTransaction txh = w3.eth.send_raw_transaction(raw).hex() if not txh.startswith("0x"): txh = "0x" + txh print("TX:", txh) w3.eth.wait_for_transaction_receipt(txh) # 3) Sign the EIP-191 binding: x402:{domain}:{invoice_id}:{tx_hash} msg = "x402:" + domain + ":" + invoice + ":" + txh.lower() sig = Account.sign_message(encode_defunct(text=msg), pk).signature.hex() if not sig.startswith("0x"): sig = "0x" + sig # 4) Claim the data (poll while the chain confirms) headers = {**UA, "X-Payment-Invoice-ID": invoice, "X-Payment-Tx-Hash": txh, "X-Payment-Signature": sig} for _ in range(40): rr = requests.get(ENDPOINT, headers=headers, timeout=30) if rr.status_code == 200: print("DATA:", rr.json()["data"]) return if rr.status_code == 402 and rr.json().get("retryable"): time.sleep(3) continue print("REJECTED:", rr.status_code, rr.text) return if __name__ == "__main__": main()