Skip to content

Paying over x402

Bruno takes x402 v2 payments over HTTP at POST /{chain}/v1/x402, using the exact scheme with EIP-3009. You send the call itself as the request, so there's no separate quote or submission step:

  1. Ask. POST the call: {"call": {"to", "data"}, "gas", "authorizationList"?}, and for a smart wallet, who pays and how it's deployed. Bruno responds with 402. The PAYMENT-REQUIRED header gives the price in USDC for that call, and maxTimeoutSeconds, the longest Bruno may hold a paid request open.
  2. Pay. Send the same request again with a PAYMENT-SIGNATURE header. It contains a transferWithAuthorization of the amount to the payee, signed for the token's EIP-712 domain (given in the requirement's extra).
  3. Settle. Bruno holds the request open until the transaction is included, then responds with 200 and a PAYMENT-RESPONSE header naming the transaction. If the call is rejected or expires first, Bruno responds with 402 and the authorization is never used. While you wait, the Location header's status URL shows the stage: waiting, sending, confirming, done.

The runner makes the transfer and the call in the same transaction, so a call that fails or never lands costs nothing. Any account can pay for any call: the call itself doesn't have to pay anything, and the payer doesn't have to be the call's sender. Paying more than the price counts as a tip, and when the queue is full, submissions are ranked by it.

Paying with x402's own Python client (x402[httpx,evm]):

from eth_account import Account
import httpx
from x402 import x402Client
from x402.http.clients.httpx import x402AsyncTransport
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import register_exact_evm_client

payer = Account.from_key(KEY)  # holds USDC, no ETH needed
x402 = x402Client()
register_exact_evm_client(x402, EthAccountSigner(payer), "eip155:*")
# a call can cost more than the client's default $1 cap
x402.set_spend_controls({"allowed_assets": [{"network": "eip155:1", "asset": USDC}],
                         "max_amount_per_payment": False})
async with httpx.AsyncClient(transport=x402AsyncTransport(x402), timeout=150) as http:
    paid = await http.post("https://bruno.mailam.app/ethereum/v1/x402",
                           json={"call": {"to": TO, "data": DATA}, "gas": hex(100_000)})

Smart wallets

A smart wallet can pay, deployed or not. A deployed one signs as ERC-1271 has it. An undeployed one signs with an ERC-6492 wrapper, as viem's and CDP's smart accounts do, and Bruno deploys it in the same transaction, before the transfer.

Its first price should include the deployment, because x402 clients pay once, at the price the 402 gives. So add payer, and deployment while the wallet is undeployed, to the request body, as for a quote:

{
  "call": {
    "to": "0x…",
    "data": "0x…"
  },
  "gas": "0x186a0",
  "payer": "0x…the wallet",
  "deployment": {
    "account": "0x…the wallet",
    "factory": "0x…its factory",
    "factoryData": "0x…"
  }
}

A payment short of its transaction's real price is answered with another 402 at the price it needs. The TypeScript client adds these fields itself, and signs again if it has to.

Notes for wallet developers

  • Set the HTTP timeout to at least maxTimeoutSeconds (120 seconds on Ethereum). httpx's default is 5 seconds.
  • Allow the token if it isn't one of x402's defaults. x402's clients only pay in their default assets. Ethereum's USDC is one of them; Sepolia's isn't.
  • Expect odd prices on Sepolia. Sepolia's USDC pools are heavily unbalanced, because faucet USDC is plentiful and Sepolia ETH is scarce, so they value ETH at around 45,000 USDC. Bruno prices calls using those pools, so a call costs tens of Sepolia USDC, which is more than a faucet usually gives at once. Swapping a little Sepolia ETH for USDC on the same pools works better.
  • Signing again doesn't pay twice. If you pay again for the same call while the first payment is still pending, the second request waits for the first, and its authorization is never used.
  • Discovery: /.well-known/x402 lists each chain's endpoint, and /openapi.json describes it with x-payment-info. The 402 response includes the bazaar extension.

The tests check conformance with x402's own SDK: its reference client pays through both relays, its models parse every response, and its facilitator code catalogs the endpoint. Where Bruno differs from the x402 specification, and why, is explained in the design.