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:
- Ask.
POSTthe call:{"call": {"to", "data"}, "gas", "authorizationList"?}, and for a smart wallet, who pays and how it's deployed. Bruno responds with402. ThePAYMENT-REQUIREDheader gives the price in USDC for that call, andmaxTimeoutSeconds, the longest Bruno may hold a paid request open. - Pay. Send the same request again with a
PAYMENT-SIGNATUREheader. It contains atransferWithAuthorizationof the amount to the payee, signed for the token's EIP-712 domain (given in the requirement'sextra). - Settle. Bruno holds the request open until the transaction is included, then responds
with
200and aPAYMENT-RESPONSEheader naming the transaction. If the call is rejected or expires first, Bruno responds with402and the authorization is never used. While you wait, theLocationheader'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/x402lists each chain's endpoint, and/openapi.jsondescribes it withx-payment-info. The 402 response includes thebazaarextension.
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.