switchboard. x402 agent payments

Tutorial progress

1

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:

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.

2

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.

3

Project setup

Create a new directory and install the dependencies:

terminal -- project setup
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.

.env
# 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:

project structure
x402-agent/
  .env
  agent.py          # main agent loop
  x402_handler.py   # intercepts 402, signs payments
  config.py         # loads .env, sets up wallet + budget
4

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.

config.py
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.

agent.py
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())
5

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.

x402_handler.py
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.

6

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:

budget configuration
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.

7

Run the agent

With your .env configured and test USDC funded, run the agent:

terminal -- run
python agent.py

Expected output:

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.

8

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: