Tutorial progress
- How x402 works
- Prerequisites
- Project setup
- Build the agent
- x402 payment handler
- Gas budget safety
- Run the agent
- Next steps
How x402 works
The x402 protocol (HTTP 402 Payment Required) is an open standard under the Linux Foundation that lets API servers charge per-request using on-chain payments. The flow is simple:
- Agent sends a request to an API endpoint (e.g., an LLM inference endpoint, a premium data feed).
- Server responds with HTTP 402 plus a
X-Paymentheader describing the price, accepted token, and chain. - Agent signs a payment using its wallet -- an EIP-712 typed signature authorizing the facilitator to settle the payment on-chain.
- Agent retries the request with the signed payment in the
X-Paymentheader. - Server verifies payment through a facilitator contract, then returns the resource.
In this tutorial, you will build a Python AI agent that autonomously handles 402 responses, signs payments from its own wallet, and enforces per-hour and per-day spend limits using switchboard's GasBudgetTracker. The agent will call a paid LLM API to answer questions, paying for each inference with USDC on Base.
Prerequisites
Before you start
- Python 3.10+ installed
- A Base Sepolia testnet wallet with some test ETH (for gas) and test USDC. Use the Alchemy faucet for ETH.
- Familiarity with
web3.py-- you should know what a private key and RPC endpoint are. - The switchboard getting started tutorial (recommended, not required).
The complete code from this tutorial is available at switchboard/examples/x402-agent.
Project setup
Create a new directory and install the dependencies:
mkdir x402-agent && cd x402-agent python -m venv .venv && source .venv/bin/activate pip install switchboard-agent web3 httpx python-dotenv
Create a .env file with your wallet credentials. Never commit this file.
# Base Sepolia testnet RPC_URL=https://sepolia.base.org PRIVATE_KEY=0x...your_testnet_private_key... CHAIN_ID=84532 # USDC on Base Sepolia USDC_ADDRESS=0x036CbD53842c5426634e7929541eC2318f3dCF7e # x402 facilitator (Base Sepolia) FACILITATOR_ADDRESS=0x...x402_facilitator_on_base_sepolia... # The paid API endpoint your agent will call PAID_API_URL=https://api.example.com/v1/inference
The project structure will look like this when we are done:
x402-agent/ .env agent.py # main agent loop x402_handler.py # intercepts 402, signs payments config.py # loads .env, sets up wallet + budget
Build the agent core
First, set up the configuration module. This loads your wallet, connects to the RPC, and initializes the switchboard gas budget tracker.
import os from dotenv import load_dotenv from web3 import Web3 from switchboard.gas_budget import GasBudgetTracker load_dotenv() # Web3 connection w3 = Web3(Web3.HTTPProvider(os.environ["RPC_URL"])) account = w3.eth.account.from_key(os.environ["PRIVATE_KEY"]) # Gas budget: limit how much the agent can spend budget = GasBudgetTracker( hourly_limit=2.0, # $2 USDC per hour daily_limit=10.0, # $10 USDC per day ) # Contract addresses USDC = os.environ["USDC_ADDRESS"] FACILITATOR = os.environ["FACILITATOR_ADDRESS"] CHAIN_ID = int(os.environ["CHAIN_ID"]) PAID_API_URL = os.environ["PAID_API_URL"] print(f"Agent wallet: {account.address}") print(f"Budget: ${budget.remaining_hourly():.2f}/hr, ${budget.remaining_daily():.2f}/day")
Now the main agent loop. This is a simple question-answering agent that calls a paid LLM endpoint. The key insight: the agent does not know or care that the API costs money. The x402 handler transparently intercepts 402 responses and pays.
import asyncio from config import PAID_API_URL, budget from x402_handler import X402HttpClient async def main(): # Create an HTTP client that auto-handles x402 payments client = X402HttpClient() questions = [ "What is the current TVL of Aave v3 on Base?", "Summarize the latest Ethereum Pectra upgrade.", "What are the top 3 yield farming strategies this week?", ] for q in questions: print(f"\n--- Question: {q}") # The agent just makes a normal API call # If the server returns 402, x402_handler pays automatically response = await client.post( PAID_API_URL, json={"prompt": q, "max_tokens": 200}, ) if response.status_code == 200: data = response.json() print(f"Answer: {data['text']}") print(f"Cost: ${response.headers.get('X-Payment-Amount', '?')}") else: print(f"Failed: HTTP {response.status_code}") # Print final budget summary print(f"\n--- Budget remaining: ${budget.remaining_hourly():.2f}/hr, ${budget.remaining_daily():.2f}/day") asyncio.run(main())
The x402 payment handler
This is the core of the tutorial. The X402HttpClient wraps httpx.AsyncClient and intercepts 402 responses. When it sees one, it parses the payment requirements from the response header, signs an EIP-712 payment authorization, and retries the request.
import json import httpx from eth_account.messages import encode_typed_data from config import account, budget, w3, USDC, FACILITATOR, CHAIN_ID from switchboard.gas_budget import BudgetExhausted def parse_payment_requirements(response: httpx.Response) -> dict: """Extract x402 payment requirements from a 402 response.""" raw = response.headers.get("X-Payment", "") if not raw: raise ValueError("402 response missing X-Payment header") reqs = json.loads(raw) return { "scheme": reqs["scheme"], # "exact" or "attest" "network": reqs["network"], # "base-sepolia" "token": reqs["maxAmountRequired"]["token"], "amount": int(reqs["maxAmountRequired"]["amount"]), "decimals": reqs["maxAmountRequired"].get("decimals", 6), "payee": reqs["payeeAddress"], "facilitator": reqs.get("facilitatorAddress", FACILITATOR), } def sign_x402_payment(reqs: dict) -> str: """Sign an EIP-712 payment authorization for the x402 facilitator.""" amount_human = reqs["amount"] / (10 ** reqs["decimals"]) # Check budget before signing try: budget.require_budget(amount_human) except BudgetExhausted as e: print(f"BUDGET BLOCKED: {e}") raise # EIP-712 typed data for x402 payment domain = { "name": "X402Payment", "version": "1", "chainId": CHAIN_ID, "verifyingContract": reqs["facilitator"], } types = { "Payment": [ {"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "token", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "nonce", "type": "uint256"}, ], } nonce = w3.eth.get_transaction_count(account.address) message = { "from": account.address, "to": reqs["payee"], "token": reqs["token"], "amount": reqs["amount"], "nonce": nonce, } signable = encode_typed_data(domain, types, message) signed = account.sign_message(signable) # Record the spend in the budget tracker budget.record_spend(amount_human) print(f"Signed x402 payment: ${amount_human:.4f} USDC to {reqs['payee'][:10]}...") return json.dumps({ "signature": signed.signature.hex(), "from": account.address, "nonce": nonce, }) class X402HttpClient: """HTTP client that transparently handles x402 payment-required responses.""" def __init__(self, max_retries: int = 1): self.client = httpx.AsyncClient(timeout=30) self.max_retries = max_retries self.total_paid = 0.0 self.payments_count = 0 async def post(self, url: str, **kwargs) -> httpx.Response: """Send POST request; if 402 returned, pay and retry.""" response = await self.client.post(url, **kwargs) if response.status_code != 402: return response # Parse payment requirements from 402 response reqs = parse_payment_requirements(response) print(f"Got 402 -- server wants ${reqs['amount'] / 10**reqs['decimals']:.4f} USDC") # Sign the payment (budget check happens inside) payment_header = sign_x402_payment(reqs) # Retry with payment header headers = {**kwargs.pop("headers", {}), "X-Payment": payment_header} paid_response = await self.client.post(url, headers=headers, **kwargs) self.payments_count += 1 amount_human = reqs["amount"] / 10**reqs["decimals"] self.total_paid += amount_human return paid_response async def get(self, url: str, **kwargs) -> httpx.Response: """Same pattern for GET requests.""" response = await self.client.get(url, **kwargs) if response.status_code != 402: return response reqs = parse_payment_requirements(response) payment_header = sign_x402_payment(reqs) headers = {**kwargs.pop("headers", {}), "X-Payment": payment_header} return await self.client.get(url, headers=headers, **kwargs)
Key design choice: The budget check happens before signing the payment. If the agent has exhausted its hourly or daily budget, the BudgetExhausted exception propagates up to the caller -- the agent simply cannot spend more than its configured limits, even if the API server demands it.
Gas budget safety
Without a budget, a buggy agent could drain its wallet in minutes. Switchboard's GasBudgetTracker uses sliding windows to enforce limits. Here is how to tune the budget for a production agent:
from switchboard.gas_budget import GasBudgetTracker # Conservative: research agent that makes a few calls per day research_budget = GasBudgetTracker( hourly_limit=1.0, # $1/hr daily_limit=5.0, # $5/day ) # Aggressive: trading agent that needs fast, frequent calls trading_budget = GasBudgetTracker( hourly_limit=50.0, # $50/hr daily_limit=500.0, # $500/day ) # Check remaining budget at any time print(f"Hourly remaining: ${research_budget.remaining_hourly():.2f}") print(f"Daily remaining: ${research_budget.remaining_daily():.2f}") # The budget auto-refills as the sliding window moves forward # If you spent $1 at 2:00pm, that spend drops out of the hourly # window at 3:00pm -- no manual reset needed.
For multi-protocol scenarios (an agent that uses x402 and on-chain escrow and MPP sessions), a single GasBudgetTracker instance tracks aggregate spend across all payment methods. Pass the same budget object to every payment handler.
Run the agent
With your .env configured and test USDC funded, run the agent:
python agent.py
Expected output:
Agent wallet: 0x1234...abcd Budget: $2.00/hr, $10.00/day --- Question: What is the current TVL of Aave v3 on Base? Got 402 -- server wants $0.0030 USDC Signed x402 payment: $0.0030 USDC to 0xabcd1234... Answer: Aave v3 on Base currently has approximately $1.2B in TVL... Cost: $0.003 --- Question: Summarize the latest Ethereum Pectra upgrade. Got 402 -- server wants $0.0030 USDC Signed x402 payment: $0.0030 USDC to 0xabcd1234... Answer: The Pectra upgrade (EIP-7702, EIP-7251, EIP-2537) went live... Cost: $0.003 --- Question: What are the top 3 yield farming strategies this week? Got 402 -- server wants $0.0030 USDC Signed x402 payment: $0.0030 USDC to 0xabcd1234... Answer: 1. Morpho Blue USDC vaults on Base (~12% APY)... Cost: $0.003 --- Budget remaining: $1.99/hr, $9.99/day
Each 402 handshake adds ~200ms of latency (signature computation + retry). In production, the facilitator settles payments asynchronously, so the agent does not wait for on-chain confirmation.
Next steps
You now have a working AI agent that pays for its own API calls using x402. Here is where to go from here:
- Add escrow payments -- combine x402 (per-request) with switchboard's agent-to-agent escrow for larger deliverables where you need refund protection.
- Multi-chain support -- the x402 payment requirements header includes a
networkfield. Extendsign_x402_paymentto support multiple chains (Base, Arbitrum, Optimism) by switching RPC and chain ID dynamically. - Plug into ElizaOS -- if you use ElizaOS as your agent framework, switchboard's upcoming ElizaOS plugin will handle x402 auto-pay natively.
- MPP sessions -- for high-frequency use (hundreds of calls/minute), switch from per-request x402 to an MPP session where you pre-authorize a spending cap and stream payments.