x402 — Base USDC¶
Pay a paywalled API with USDC on Base (or Base Sepolia testnet) using the x402 protocol.
Prerequisites¶
| Requirement | Notes |
|---|---|
| Base wallet | Any eth_account.LocalAccount — hardware wallets not yet supported |
| Base USDC balance | Mainnet: USDC on 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Testnet: Circle faucet at faucet.circle.com |
| ETH for gas | Small amount for EIP-2612 approval (if first use) and gas |
| x402-capable endpoint | Your server or a third-party API that speaks x402 |
Environment variables¶
export WALLET_PK=0x<your-private-key> # 32-byte hex private key
export MERCHANT_ENDPOINT=https://your-api.com/paid
Never commit private keys
Load keys from environment variables, AWS Secrets Manager, or a vault.
The eth_account.Account.from_key() call accepts raw hex or a bytes object.
Testnet (Base Sepolia)¶
import asyncio
import os
from eth_account import Account
from routeweiler import Routeweiler, Funding
from routeweiler.trace.sink_sqlite import TraceSink
async def main():
signer = Account.from_key(os.environ["WALLET_PK"])
async with Routeweiler(
funding=[Funding.base_sepolia_usdc(wallet=signer)],
trace_sink=TraceSink.sqlite("rw.db"),
agent_id="quickstart-agent",
) as client:
response = await client.get(os.environ["MERCHANT_ENDPOINT"])
print(f"HTTP {response.status_code}: {response.json()}")
asyncio.run(main())
Mainnet (Base)¶
Switch to Funding.base_usdc(wallet=signer) — everything else is identical.
async with Routeweiler(
funding=[Funding.base_usdc(wallet=signer)],
trace_sink=TraceSink.sqlite("rw.db"),
) as client:
response = await client.get("https://your-api.com/paid-endpoint")
With a budget envelope¶
Cap total spend to $10.00 per session:
from routeweiler import Routeweiler, Funding, BudgetEnvelope
from routeweiler.trace.sink_sqlite import TraceSink
async with Routeweiler(
funding=[Funding.base_usdc(wallet=signer)],
trace_sink=TraceSink.sqlite("rw.db"),
budget_envelope=BudgetEnvelope(
id="session-001",
cap_minor_units=1_000, # $10.00 in cents
cap_currency="usd",
allowed_rails=["x402"],
ttl_seconds=3_600,
),
) as client:
for url in urls_to_fetch:
response = await client.get(url)
When the envelope is exhausted, BudgetExceededError is raised instead of paying.
What the flow looks like¶
GET https://your-api.com/paid-endpoint
← 402 Payment Required
PAYMENT-REQUIRED: <base64 JSON with accepts[]>
Routeweiler:
1. Parses x402 challenge (network="base", asset=USDC, amount=N)
2. Checks policy (allow? cap?)
3. Checks budget envelope (enough remaining?)
4. Signs ERC-3009 transferWithAuthorization locally
5. Retries with PAYMENT-SIGNATURE header
GET https://your-api.com/paid-endpoint
PAYMENT-SIGNATURE: <base64 signed payload>
← 200 OK
PAYMENT-RESPONSE: <base64 settlement proof with txHash>
What to check in the trace¶
import sqlite3, json
conn = sqlite3.connect("rw.db")
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM trace_events WHERE selected_rail = 'x402'"
).fetchall()
conn.close()
for row in rows:
payload = json.loads(row["payload"])
payment = payload["payment"]
print(f"Tx: {payment['proofValue']}")
print(f"Amount: {payment['amountNative']} base units = {payment['amountEnvelope']} USD")
print(f"Settled in: {payment['settlementLatencyMs']} ms")
proofValue is the on-chain transaction hash (0x<64 hex chars>).
You can look it up on Basescan (mainnet)
or Sepolia Basescan (testnet).
Error handling¶
from routeweiler import (
BudgetExceededError,
PolicyDeniedError,
NoFeasibleRailError,
SigningError,
PostCommitPaymentError,
)
try:
response = await client.get(url)
except BudgetExceededError:
print("Envelope cap hit — no more payments this session.")
except PolicyDeniedError as exc:
print(f"Policy blocked the payment: {exc.reason}")
except NoFeasibleRailError:
print("No usable rail (check funding source or policy).")
except SigningError as exc:
print(f"EVM signing failed: {exc}")
except PostCommitPaymentError:
# The payment went on-chain but the server returned an error.
# Check the trace for the tx hash to understand what happened.
print("Post-commit error — payment may have landed on-chain.")
See also¶
- x402 spec
Funding.base_usdc/Funding.base_sepolia_usdcin API Reference- Concepts: Payment Rails