Plan: Bruno as an x402 facilitator¶
Bruno settling x402 payments for other sellers: POST /verify, POST /settle,
GET /supported, x402 v2's facilitator interface (§7), so a resource server anywhere can
point its x402 middleware at Bruno instead of CDP. Bruno already does a facilitator's work
for its own endpoint (verifying an EIP-3009 authorization, refusing with x402's codes,
settling it on chain); this opens it to anyone, permissionlessly: no account, no API key.
Normative text (scratchpad/x402spec/, coinbase/x402 at 0e6e8c5): x402 v2 §6.1 (payment
flows), §7 (facilitator interface), §8 (discovery); schemes/exact/scheme_exact.md and
scheme_exact_evm.md; schemes/auth-capture/scheme_auth_capture.md and
scheme_auth_capture_evm.md; extensions/bazaar.md.
Where Bruno would stand out¶
- Ethereum mainnet. CDP's facilitator settles on Base, Polygon, Arbitrum, Monad and World, not Ethereum.
- Settlement through bundles: a settlement that would fail never lands, so a facilitator running on Bruno pays nothing for failures (insufficient funds, a nonce used meanwhile).
- Permissionless: no account or key for the seller;
GET /supportedis the whole onboarding. - Its own Bazaar: §8's
GET /discovery/resourceslists what a facilitator has settled for, so sellers settling through Bruno are discoverable through Bruno.
Getting paid: the fee question¶
exact has no fee. Its summary table: "Fees: None"; settlement "MUST produce exactly one
identifiable transfer of amount of asset to payTo". The facilitator pays the settlement's
gas (the client signs an authorization, not a transaction: scheme_exact.md, fee payer
"sponsored by the facilitator, or self-funded by the payer", and a 3009 payer can't fund it).
So with exact a facilitator either sponsors or bills the seller off chain. CDP bills (an API
key account; 1,000 settlements a month free, then $0.001 each), which Bruno can't without
accounts.
auth-capture has a fee in the scheme. Built on base/commerce-payments
(AuthCaptureEscrow); the facilitator's terms in /supported's extra: feeRecipient,
minFeeBps, maxFeeBps, which the server copies verbatim into its requirements. The client's
signature commits to the bounds (PaymentInfo); the fee is taken on chain at charge or
capture (v1.1: an absolute feeAmount within the bounds). Equal bounds fix it. bps are the
spec's unit on the wire; Bruno's configuration keeps Decimal multipliers and converts.
Its costs: a lifecycle (authorize, capture, void, refund, reclaim, two deadlines), not one
transfer; charge/capture need the seller's consent (a receiver authorizer's signature, or
delegated to the facilitator: trust within the client's bounds); the escrow deployed on the
chain; clients and sellers that speak it (most pay exact today).
Checked 2026-10-23: the escrow is on Base only. eth_getCode on the v1.1 and v1.0 escrows
and 3009 collectors (scratchpad/escrow_check.sh): code on Base and Base Sepolia; none on
Ethereum, Sepolia, Optimism, Arbitrum, Polygon, Unichain, Linea, BNB Chain, World Chain,
Avalanche. The README says CREATE2, "the same address on Base Mainnet and Base Sepolia"; the
repo's Deploy.s.sol uses plain new. A facilitator MUST reject any escrow but the two
canonical addresses, so auth-capture on another chain needs the canonical set deployed
there: by whom, and whether anyone can (the factory, salt and exact bytecode used on Base) is
to find from Base's creation transactions.
Within exact itself there are ways to take a fee with the seller's cooperation: below.
Fees within exact: the options¶
exact binds one thing: the client signs a transfer of exactly amount to payTo, and
"operations incidental to making that payment, such as fee payment … do not disqualify it"
(scheme_exact.md). So a fee is possible if payTo isn't the seller's final address but one
the settlement forwards from, net of the fee. It takes the seller's cooperation: the seller
(the paid service) publishes a payTo it got from, or agreed with, the facilitator. Each
option below is that, differing in what payTo is and so in what the client's signature
binds, who holds the funds, and how many transfers a settlement makes.
What matters in comparing them:
- Binding: the client's signature commits only to
payTo. If the seller's destination isn't determined bypayTo, it arrives in the/settlerequest as unsigned input, and anyone holding a client's payload (a proxy, a log, a middleman) could settle it to a destination of their own. Plainexactcan't be redirected so. - Custody: whether funds ever rest where the facilitator could take them.
- Transfers per settlement: a USDC transfer is ~30–60k gas; creating a balance slot (a zero balance made non-zero) costs more again (EIP-8037's state cost on Amsterdam).
- Failures: the facilitator pays the settlement's gas, and a transfer that fails (insufficient funds, a nonce used meanwhile) reverts and spends it for nothing: bundles where gas is real (Ethereum), the public mempool where it isn't (L2s).
A. A shared long-term facilitator key. payTo is one facilitator account for every
seller. Settlement: the client's transfer in, then the amount less the fee out to the seller:
two transfers, the fee simply staying. Simplest, and atomic if both are in one transaction.
But the destination isn't bound (any payload can be settled to anyone), and it's custodial:
every seller's funds pass through one key, and sellers trust the facilitator to forward.
B. A key derived per seller (P·H(destination, …), stealth-address style, from the
facilitator's public key P). payTo commits to the destination, so a payload can't be
redirected by a third party. But the facilitator holds the derived private key
(sk·H(…)) and could sweep it anywhere: still custodial, the seller trusting the facilitator,
and each derived account needs gas or a sweep path. Transfers as A.
C. Three transfers to fixed addresses. payTo as A or B, and settlement sends the fee to
the facilitator's payee and the rest to the seller, leaving payTo empty: three transfers,
and payTo's balance slot created afresh each time. Clean accounting, the most gas; binding
and custody as A or B.
D. A CREATE2 forwarder, no key. payTo = CREATE2(deployer, H(destination, token, fee
bounds), forwarder code): a pure function anyone can recompute, so the seller verifies its
payTo commits to its destination, and "negotiating" is computing an address. The code can
only send to its destination, keeping a fee within the bounds baked into it (auth-capture's
client-bounded fee, carried by the address instead of an escrow, so no escrow per chain).
Settlement, one transaction from Bruno's runner: the client's transfer into the forwarder,
then the forwarder's transfer of the amount less the fee to the destination: two transfers,
the runner measuring what arrived by balance difference, as it measures payments now.
- The fee stays in the forwarder: a payment arrives and is split in the same transaction, so between transactions its balance is only accumulated fees, swept to Bruno now and then as a third leg of a settlement that makes it worth it. Its balance never returns to zero, so a payment into it never creates its balance slot (the payee's kept unit, again).
- Deployed once, kept: the runner self-destructs because it's single-use; a forwarder is reused by its seller, and creating an account costs 183,600 gas on Amsterdam each time. Deployed by the first settlement, counterfactual until then.
- Fee bounds: fixed in the address while gas moves with the base fee, so a fraction of
amountplus a fixed floor; the facilitator picks the fee within them per settlement, and/verifyrefuses a payment whose fee can't cover its settlement rather than settle at a loss. - Non-custodial and bound; the costs: a contract to write, audit and vendor; a deployment per
seller; sellers changing their
payTo.
E. 402 on /settle (outside the facilitator spec). The seller's server pays Bruno per
settlement by x402, and Bruno's runner makes both transfers (the client's to the seller, the
seller's fee to Bruno) in one transaction through a bundle, so neither lands without the
other. payTo stays the seller's own address: bound, non-custodial, no contract. But no seller
SDK expects a 402 from its facilitator, and the seller needs funds and a signer for the fee.
F. auth-capture. The spec's own fee (above): client-signed bounds, on chain; the escrow on
Base only, a lifecycle to implement, clients and sellers that speak it.
| Destination bound | Custody | Transfers | Needs | |
|---|---|---|---|---|
| A shared key | no | facilitator | 2 | nothing |
| B derived key | yes | facilitator | 2 | a key per seller |
| C three transfers | as A/B | as A/B | 3 | nothing |
| D forwarder | yes | none | 2 (+ a sweep now and then) | a contract, a deployment per seller |
E 402 on /settle |
yes | none | 2 | seller SDK support, the seller's own signer |
F auth-capture |
yes | escrow (client-bounded) | escrow lifecycle | the escrow on the chain, client support |
Decisions (proposed)¶
exactfirst, sponsored, where settlement is cheap (Base, other L2s:upfront-style settlement, no bundler guarantee needed for a lone transfer); on Ethereum,exactsponsored only within a configured budget, since each settlement costs real gas.auth-capturefor a real fee, on Base now, elsewhere once the canonical escrow is.- Settlement: Bruno's existing 3009 settlement, as a plain transaction (not a runner).
/settleanswers once included, with the hash. Where Bruno sends through bundles it signs the settlement once, a fee cap covering its window, so the hash is fixed at broadcast;settlement_pending(which MUST carry that hash) if inclusion can't be confirmed in time. /verify: read-only, astake/refusaldo now: signature, balance, nonce unused, validity window, simulation.signers: the accounts settling, per network (eip155:<id>).- Bazaar: the
bazaarextension's metadata recorded from settled payments and served at/discovery/resources. - Shared with
x402-async.md: Bruno's ownupfrontpayments settle through the same code, its own facilitator rather than CDP's where it runs.
Open questions¶
- Sponsoring
exacton Ethereum: a monthly budget, a per-seller limit (bypayTo), or not at all? auth-capture: worth its lifecycle now, on Base only, or wait for the escrow to reach other chains?- Deploying the canonical escrow on Ethereum ourselves, if Base's deployment is reproducible (CREATE2 factory and salt): ours to do, or Base's?
- Which fee option (A–F): D or E for permissionless and non-custodial; F where the escrow is; A–C only if custody is acceptable. Possibly D and F both.
- Abuse: a free facilitator is a gas faucet for anyone settling dust; rate limits by
payToand a minimum amount per network?