Skip to content

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 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Testnet: 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