Plan: ERC-4337, bundlers and paymasters¶
Bruno takes a different path from ERC-4337. A wallet signs an EIP-3009 transfer (checked by ERC-1271 if the wallet is a contract), and Bruno's runner sends the call and takes the payment in the same transaction. The smart wallets in the smart wallets plan mostly act through UserOps instead: a bundler puts them on-chain through the EntryPoint, and a paymaster can pay their gas, taking a token for it.
This plan records how that ecosystem works, where Bruno differs, and whether Bruno should meet it anywhere. It's research; nothing here is decided.
How ERC-4337 works¶
- UserOps and the EntryPoint. A wallet signs a UserOp and sends it to a bundler with
eth_sendUserOperation(ERC-7769). The bundler checks it against ERC-7562's validation rules and puts it in ahandleOpscall to the EntryPoint, a singleton contract. The EntryPoint pays the bundler's chosenbeneficiaryall the gas fees collected in the bundle. - Bundlers and builders. ERC-4337 allows a bundler to be a block builder itself, or to work with builders through infrastructure such as mev-boost. Alternative mempools, with different validation rules, are out of its scope.
- Paymasters. A paymaster agrees to pay in
validatePaymasterUserOp, and can return a context so the EntryPoint calls itspostOpafter the execution. InpostOp'sopRevertedmode, "the paymaster still has to pay for gas". A paymaster holds a deposit in the EntryPoint, and needs a stake to use some features under ERC-7562. - EIP-7702. From EntryPoint v0.8, a UserOp can carry an EIP-7702 authorization (ERC-7769's
eip7702Auth, with the0x7702factory marker), so an upgraded key can use bundlers and paymasters.
The EntryPoint versions, each with code on Ethereum (checked 2026-10-28):
| Version | Address | Notes |
|---|---|---|
| v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
paymasterAndData in one field |
| v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
PackedUserOperation; paymaster gas limits split out |
| v0.8 | 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 |
EIP-7702 authorizations; UserOp hash as ERC-712 |
| v0.9 | 0x433709009B8330FDa32311DF1C2AFA402eD8D009 |
ABI-compatible with v0.8; to confirm against the release notes |
Sources: ERC-4337, ERC-7769, ERC-7562, eth-infinitism/account-abstraction releases, eth-infinitism/bundler, EntryPoint v0.8 released, Candide on EntryPoint v0.9.
How apps reach a paymaster¶
EIP-5792's wallet_sendCalls takes a paymasterService capability, a URL and a context. The
wallet calls that service with ERC-7677's two methods: pm_getPaymasterStubData (placeholder
fields for estimating gas) and pm_getPaymasterData (the real fields, before the UserOp goes
to a bundler). Wallets advertise the capability per chain in wallet_getCapabilities, and may
fall back to another paymaster than the one the app named. ERC-7677 describes v0.6 and v0.7 in
detail, and says the API can be used with other versions.
So any wallet that supports paymasterService can use a paymaster without wallet-specific code.
Paymasters that take a token¶
Circle Paymaster¶
Read from its verified source (TokenPaymasterV08, the implementation behind the proxy at
0x0578cFB2…700Ec on Ethereum) and its settings read on 2026-10-28:
- Where it runs. v0.7 on Arbitrum and Base; v0.8 on Arbitrum, Avalanche, Base, Ethereum, Optimism, Polygon and Unichain, at the same address on each. USDC only. No account or API key.
- Permit.
paymasterAndDatacan carry an EIP-2612 permit (amount and signature, deadline the maximum). The paymaster tries it and carries on if it fails, taking the permit as already used, since it can be front-run from the mempool. Otherwise the account approves the paymaster first. - Price.
latestRoundData()from an oracle. On Ethereum it's Chainlink's ETH / USD feed (0x5f4eC3Df…5b8419), read as ETH in USDC: USDC is taken to be worth a dollar. The round'supdatedAtisn't checked, so a stale price is used as it is; only a price of zero or less reverts. - Charge. In validation it pulls a prefund from the account: the UserOp's
maxCost, plusadditionalGasChargegas atmaxFeePerGas, at the oracle's price, plus the spread. InpostOpit charges the actual gas cost the same way, and adds 10% of the unusedpostOpgas limit above 40,000 as a penalty. Then it refunds the rest. The result is(gas cost in wei × price) / 10¹⁸ + 1, so it rounds up by 0.000001 USDC. - Reverts.
postOpignores its mode, so a UserOp whose call reverts is charged the same as one that succeeds. - The surcharge. The docs say 10% on Arbitrum and Base. On-chain,
feeSpreadwas 0 on Ethereum, Base, Arbitrum and Unichain (v0.8), and on Base and Arbitrum (v0.7), on 2026-10-28. What's charged on top isadditionalGasCharge: 35,000 gas on each. - The USDC it takes is swapped for ETH by a privileged swapper, through a Uniswap router, at the oracle's price less a slippage the swapper sets.
- Controls. The owner can pause it, deny senders, and change the oracle, the spread and the extra gas. It's an upgradeable (UUPS) proxy.
Sources: Circle Paymaster, its addresses and events, the proxy on Etherscan, the implementation's verified source.
Pimlico's singleton paymaster¶
Read from its source (SingletonPaymasterV7, SingletonPaymasterV8, BaseSingletonPaymaster;
the older ERC-20 paymaster is deprecated). One contract with two modes:
- Verifying mode sponsors: the app pays.
- ERC-20 mode charges the account in a token. Pimlico's signer signs the terms into
paymasterAndData, per UserOp: exchangeRate: tokens per ETH, set by Pimlico's API. Nothing is read on-chain; any markup is in this rate.validUntilandvalidAfter: the quote's window.postOpGas: the gas its own token transfer costs, charged on top.treasury: where the tokens go.- optional
preFundInToken, taken in validation (at most the UserOp'smaxCostat the rate),constantFee, a flat fee on top, andrecipient. - Charge. In
postOp:(actual gas cost + postOpGas × fee per gas) × exchangeRate / 10¹⁸ + constantFee. On v0.7 it adds 10% of the unused execution gas limit; v0.8 doesn't. It then settles the difference from the prefund: pulling more from the account, or refunding from the treasury, which must have approved the paymaster. With arecipientset, a prefund above the cost goes to the recipient, not back to the account. - Reverts.
postOpignores its mode here too: a reverted UserOp is charged. - Approval. The account approves the paymaster beforehand, for at least the UserOp's
maxCostin tokens. There's no permit. - Bundlers. It can be limited to an allowlist of bundlers (by
tx.origin), unless the UserOp's data allows all. - EIP-7702. v0.8 binds its signature to the delegate an EIP-7702 UserOp names.
Sources: pimlicolabs/singleton-paymaster (V7, V8, base), Pimlico's ERC-20 paymaster architecture.
Sponsoring paymasters¶
Coinbase CDP, Alchemy, Pimlico (in verifying mode) and others: an app pays its users' gas under a policy, and the user pays nothing. To record: their policies and prices, as the other alternative to Bruno for an app.
Where Bruno differs¶
As facts, for the comparison the docs will need:
- What the wallet signs. For Bruno: one EIP-3009 transfer, and whatever the call needs. For ERC-4337: a UserOp, checked by the account's own validation, plus whatever the paymaster needs (a permit, an approval).
- Who can use it. Bruno serves keys (through EIP-7702 and its runner) and smart wallets alike, with any token that has EIP-3009. Circle Paymaster needs an ERC-4337 account or an upgraded key, and takes only USDC.
- A reverting call. Bruno relies on builders dropping a transaction that would revert. In ERC-4337 a UserOp whose execution reverts is still included, and both Circle's and Pimlico's paymasters charge it in full.
- Prices. Bruno measures prices from on-chain pools. Circle reads Chainlink's ETH / USD feed, taking USDC at a dollar and not checking the round's age. Pimlico's API sets a rate per UserOp and signs it.
- What the operator holds. Bruno holds no deposit or stake in a contract: the runner is new each transaction. A paymaster holds a deposit in the EntryPoint, and maybe a stake.
- Chains. ERC-4337 works on any chain with an EntryPoint. Bruno's atomic flows need the
bundler guarantee (more chains), and the
upfrontflow doesn't.
Ways Bruno could meet the ecosystem¶
Each is open. For each, what it would take is to research.
- Stay outside, as now, and explain the difference in the docs for someone choosing between them.
- Bruno as an ERC-7677 paymaster service. Any wallet that supports
paymasterServicecould pay through Bruno with no SDK of ours. It needs a paymaster contract, a deposit in each EntryPoint version, and a charge inpostOp. That's a different trust model from the runner's: the paymaster pays even when the UserOp reverts. Not taken (decided 2026-10-29): a paymaster is the part of ERC-4337 Bruno exists to avoid, a deposit held in a contract and a charge even when the op fails. - Bruno as a bundler (
eth_sendUserOperation). Bruno would take UserOps and sendhandleOpsthrough its builders, with the revert guarantee it already relies on, and be paid as the beneficiary. The UserOp's gas is paid by the account's deposit or by a paymaster such as Circle's. To find: whether the guarantee changes what a bundler has to simulate under ERC-7562, and how bundlers share a mempool. - Native account abstraction. EIP-8141's frame transactions are the protocol's own path, and would change all of the above. Watched rather than planned.
- A UserOp as a call that pays for itself (below). The relay unchanged; a client adapter.
Source: EIP-8141.
A UserOp as a call that pays for itself (design, 2026-10-28)¶
Any ERC-4337 account can send its calls through any Bruno relay, as an ordinary submission the relay already takes ("the call pays for itself"), with nothing on the relay changed. Option 3 without a bundler: Bruno's guarantee stands in for a bundler's simulation rules.
- The call is the EntryPoint's
handleOps([op], beneficiary), from the runner. - The op's fees are zero (
maxFeePerGas,maxPriorityFeePerGas): its required prefund is zero, so the account needs no ETH and no deposit, and no paymaster takes part. The relay pays the transaction's gas, as for any call. - The payment is inside the op: its batch ends with the token's transfer of the quoted amount to the relay's payee. The runner checks the payee gained it, as it checks any call.
- Paid only if it succeeds. An op whose execution reverts doesn't revert
handleOps(it's marked failed); but an account whose batch reverts as a whole on any failing call takes the payment back with it, so the payee gains nothing, the runner reverts, and nothing is included. Kernel's default mode, Coinbase Smart Wallet's and Light Account'sexecuteBatchand Safe's MultiSend all do; an account that carries on past a failure is its own choice. A paymaster charges for a failed op. - Permissionless: no API key, bundler stake or paymaster deposit; any relay, its price its quote.
EntryPoint v0.7, as written (eth-infinitism v0.7.0, core/EntryPoint.sol): handleOps is
public nonReentrant, with no check of tx.origin or of the caller's code, so the runner may
call it; the required prefund is the gas limits times maxFeePerGas, and nothing refuses zero;
a failed execution emits UserOperationEvent with success false rather than reverting;
_compensate pays the beneficiary even zero, by a call that must succeed and to an address not
zero (the payee does). innerHandleOp refuses ("AA95 out of gas", reverting the bundle) if the
call hasn't the gas the op asks: the call's gas covers the op's limits.
The client side, in @mailam/bruno, coupled to the standards and to no library's types:
viem's change between versions and break their dependents, so the client keeps none of
them (as viemSigner takes any account of the shape it needs, not viem's Account).
- The core works on ERC-4337's own data (v0.7's packed op, its hash) and Bruno's API:
quote the
handleOpscall, submit it as a call that pays for itself, read its status. - The surface is an EIP-1193 provider answering ERC-7769's methods
(
eth_sendUserOperation,eth_estimateUserOperationGas,eth_getUserOperationReceipt,eth_getUserOperationByHash,eth_supportedEntryPoints,eth_chainId):request({ method, params }), which every ERC-4337 library takes as its bundler, viem'scustom(), ZeroDev'sbundlerTransportand permissionless.js alike. viem's bundler client is the layer ZeroDev's and permissionless.js's clients are built on, so one provider serves all of them, and every account they make (Kernel, Coinbase Smart Wallet, Safe, Light Account). - The payment goes into the op before it's signed, the one step that touches a library's
own pipeline. The floor: the caller adds
bruno.payment(...), a plain{ to, data, value }call, to its calls, no hook. Above it, aprepareUserOperationthat viem's and ZeroDev's clients look up on the client (viem 2.38'sgetAction) adds it for them, typed by the shape it needs and tested against the versionsnpm-testpins. - Its op: zero fees, its gas limits estimated (below), the account's nonce, and its factory and data while it's undeployed (the op's own deployment: no ERC-6492 needed).
ZeroDev's SDK is coupled to ZeroDev only by default: createKernelAccountClient's
bundlerTransport is any viem transport, the paymaster is optional, and its one ZeroDev RPC,
zd_getUserOperationGasPrice, is replaced by passing userOperation.estimateFeesPerGas. The
project ID is only part of ZeroDev's URLs. AgentKit's ZeroDevWalletProvider is tighter: it
sends its own transactions as ZeroDev intents and doesn't expose its account, so for AgentKit the
action provider stays the way in.
Open points, to settle before building:
- Gas limits: settled on anvil (2026-10-28). EntryPoint v0.7's
EntryPointSimulations, put at the EntryPoint's address byeth_call's state override, runssimulateHandleOpwith the account's stub signature and reports the signature failed (invalidationData) rather than reverting. Simulated with a fee of 1 wei, funded by a deposit for the account in the EntryPoint's storage (the same override; itsdepositsmapping, slot 0),paidis the gas used. Not by giving the account ETH: an undeployed account would then exist already, and Amsterdam's state gas (EIP-8037) charges creating one, so the estimate fell short there ("AA26 over verificationGasLimit"), found by the tests on Amsterdam. Execution is then(paid − preOpGas − 10% × the simulated callGasLimit) ÷ 0.9, taking out v0.7's penalty on unused call gas. The limits are those plus 10%. An undeployed Kernel paying 1 USDC in its op:preOpGas293,361, execution 39,467; limits 322,697 and 43,413; the signed op'shandleOpsestimated at 378,880 and used 370,029 (2.4% over), deployed and paid. The limits set the price: the EntryPoint wants the op's whole limits available (AA95), so the call's gas follows them, not what's used.EntryPointSimulationsis never deployed: its code is eth-infinitism's published build (@account-abstraction/contracts0.7.0), whoseEntryPointmatches Ethereum's byte for byte but the SenderCreator immutable. Oneeth_callwith overrides, which public nodes take. - The quote and the signature: one signature. The payment's amount doesn't move the gas
(the same calldata, the same transfer), so: simulate with a placeholder amount, quote the
handleOpscall at the limits, put the quoted amount in, sign once. If the relay asks more when it's taken, sign again, as x402 does. - Where the simulation runs: the relay (decided 2026-10-28). It answers ERC-7769's
eth_estimateUserOperationGasitself, on the chain's JSON-RPC endpoint and atPOST /v1/user-operations/gas, both relays: the client carries no EVM code, and a relay answers only the methods it defines, so it's no gateway to its nodes. Done (2026-10-29): the estimate,eth_sendUserOperation(what an op pays found by simulating it, the payee's balances read before and after; a client signs again at what underpaid asks),eth_getUserOperationReceipt(read from the transaction when asked, nothing stored),eth_supportedEntryPointsandeth_chainId: the relay's URL is a bundler transport. viem's owncreateBundlerClient, the relay's URL its transport and a Kernel its account, sends and waits for its receipt in the npm tests, against both relays (npm-userops/userop-viem.ts), and so does ZeroDev'screateKernelAccountClient, the relay itsbundlerTransport, no project ID. Both are the recipe in the user docs (using/user-operations/). - EntryPoint versions: v0.7 and v0.8 (EIP-7702 accounts, its EIP-712 op hash) done; v0.6 not planned, and v0.9 (which viem's Simple7702 account can already name) not yet looked at.
Two ways to pay a relay as a bundler (design, open, 2026-10-29)¶
The aim: anyone using ERC-4337's tools as they are points them at a relay, and it works as a permissionless bundler, with nothing of Bruno's in their code; and an account holding only a token, no ETH and no paymaster, can still pay by a call in its op at the relay's price. Both, without either breaking the other.
- Paid by its fees, as at any bundler. The op's fees are above zero; the EntryPoint pays
the beneficiary, the relay's payee, in ETH, from the account's deposit or what it sends in
validateUserOp. The runner sees it as ETH the payee gained, so it's checked as any payment. - Paid by a call in it (built). Its fees zero: its calls pay the payee a token the relay takes.
- No paymaster of Bruno's (decided). Another's, such as Circle's, is the account's
business: its deposit pays the EntryPoint, which pays the relay as for any op paid by its fees
(built: step 3 below). Nor a token's address as
paymaster, to say what an op pays: the field is signed (inpaymasterAndData) and the EntryPoint calls it (validatePaymasterUserOp, AA33), and the token is found by simulating the op anyway.
The op says which it is: fees above zero, its fees pay, in ETH; zero, its calls pay, in a token. Never both added up.
Where they meet:
- The gas price methods choose before there's an op.
zd_getUserOperationGasPrice(which ZeroDev's client calls unless givenestimateFeesPerGas) andpimlico_getUserOperationGasPricetake no parameters. Real prices make the bundler work out of the box and an account with only a token fail its prefund (AA21); zeros the reverse. Leaning: real prices, and whoever adds a payment call (already Bruno's own code) sets zero fees too. - viem asks its node, not the bundler (doubling the fees), unless given no node client, when it reads blocks from the bundler, which a relay doesn't serve. So for viem the work is that the relay's price fits under that, not a method.
preVerificationGasis zero for an op paying by a call; for one paying by its fees it has to carry what the EntryPoint doesn't measure: the transaction's intrinsic gas, its calldata, the runner (about 110k). The estimate answers by the op's fees.- What its fees pay is the gas used (and 10% of execution gas unused), times
min(maxFeePerGas, maxPriorityFeePerGas + basefee), while a quote is on the planned gas. The gas price answers are then the relay's terms: the priority fee covers the builder's tip and the relay's margin, with room for the base fee moving before the op is sent. - A failed op: paid for when its fees pay (
handleOpscompensates the beneficiary regardless); not when its calls pay and its batch reverts as a whole. Either way nothing is included unless the payee gains. Decided (2026-10-29): an op paying by its fees keeps the EntryPoint's behaviour, charged for a failed op as at any bundler, and the docs say so. - Estimating: an op paying by a call is simulated with a deposit put in the EntryPoint's storage; one paying by its fees isn't, or a prefund it can't pay would pass the estimate and fail when sent.
- No mempool rules needed: ERC-7562's validation and reputation rules protect a bundler from ops that fail after simulation and cost it gas. A relay's reverting transaction is dropped by the builder and costs nothing; what's left is the cost of simulating, rate limits.
- The price before signing, for an op paying by a call. Decided (2026-10-30):
postpaid_quoteUserOperation, the op with a placeholder payment quoted in each token before it's signed, so it's signed once. A field on the estimate was no use: viem keeps only the limits it knows, and prepares the op's calls before estimating. The underpaid answer stays for a price risen since. Neither this nor the retry it replaces works with the URL alone: ERC-4337 has no standard way to pay a bundler in a token, so the client adds the payment and asks its price.
Open: how far over the next block's price the methods answer (4).
From the user's side (2026-10-29)¶
- Holding USDC, using Circle's paymaster (Circle's quickstart: viem's bundler client, a
paymasterwhosegetPaymasterDatasigns an EIP-2612 permit to it, fees frompimlico_getUserOperationGasPriceon the bundler, Pimlico's public bundler): changes only the bundler's URL. For that, the relay answerspimlico_getUserOperationGasPrice, takes an op with fees and another's paymaster, and estimates it with the paymaster's data. Circle's paymaster is on EntryPoint v0.7 only on Arbitrum and Base; on Ethereum and Sepolia it's v0.8, so there this needs v0.8. Its v0.8 quickstart is an EIP-7702 account (toSimple7702SmartAccount), the authorization sent with the op: a type-4 transaction, as the relay sends already. (Quickstart, addresses.) - Holding ETH: changes only the URL; viem's fees from its node, doubled, cover the price.
- Holding neither, only a token the relay takes (WETH, USDT, …): the client is set up as
for any bundler; its
sendUserOperationdiffers in three things: fees zero, a last call paying the payee, at the relay's price. Asnpm-userops/userop-*.tsdoes now. The gas price methods' answers don't reach it, its fees being given, so the two ways don't meet there: the methods answer real prices (settles 1). Tokens' paymasters (Pimlico's, ZeroDev's, Alchemy's) each want an API key and sign their rate.
Building both¶
- Paid by its fees, v0.7, no paymaster: done (2026-10-29). Fees above zero taken, the
payment the ETH the payee gains; the estimate unfunded and its
preVerificationGasthe rest of the transaction's gas (planned as any call's, less what the op uses);pimlico_getUserOperationGasPriceandzd_getUserOperationGasPriceanswer a quote's price a gas as both fees, a flat price to EntryPoint v0.7 whatever the base fee. Found on the way: - viem's own fees (its node's, doubled) come to the base fee and twice the tip, which ties
with the relay's price (the base fee, the tip, and its fee of a tip) and fell 4% short. So
the estimate's
preVerificationGasis set from the fees the op offers: as much gas as, at its price in the next block, comes to a quote of its transaction (as L2 bundlers put the L1 fee in it, at the op's price). viem then needs nothing but the URL; ZeroDev's client askszd_getUserOperationGasPriceby default. - An op whose price isn't flat pays by the base fee of the block it lands in, while a node
runs
eth_callat a base fee of 0. So the runner checks what its fees pay at any base fee (its gas atmin(maxFee, priority fee)), and the relay's economics (the underpaid check, the per-block check, ranking) what they pay in the block it's sent for. EntryPointSimulationscharged an op 0.4% more gas than the EntryPoint did (458,215 against 456,370), so a payment read from itspaidfailed the runner's check. A send now runs the op through the EntryPoint's ownhandleOps, from the balances helper, which then reads the payee's balances: the ETH its fees paid is read as any token's payment, and an account refusing the signature is the EntryPoint's AA24.- What's left of the account's prefund stays its deposit in the EntryPoint, not its ETH. Tested: viem's and ZeroDev's clients from an account holding ETH, the scripts in the user docs, both relays.
- EntryPoint v0.8: done (2026-10-29). Its op hash (EIP-712, an EIP-7702 account's
delegate in place of its init code), its
EntryPointSimulations(0.8.0's published build), an op's EIP-7702 authorization (eip7702Auth) carried in the relay's transaction; both versions ineth_supportedEntryPoints, v0.8 first. Found on the way: - v0.8 dropped v0.7's flat price for equal fees: its price is always
min(maxFee, priority fee + base fee), the same number, but the min's two branches cost a few gas apart (15), so the gas an op is charged moves with the base fee. A send measured at a base fee of 0 set a floor the real block missed, and the builder dropped it every block. So a send runs the op twice, at 0 and at the node's base fee (a fee given by an account given ETH by override; base fee overrides are ignored by three of the four mainnet nodes configured), and the runner checks the lesser. - The transaction carrying an authorization warms its account (EIP-7702), so the op is charged about 2,500 gas less than a run that only overrides the account's code. A send's runs give the account in an access list.
- viem (2.38.3) estimates with a stub authorization and sends its stub too unless given a
signed one: the user docs' script signs it while the EOA isn't delegated.
Tested: an EOA delegating in its first op, paying by its calls (viem's stub authorization
estimated) or by its fees (viem's own, not flat); one delegated already, bringing none; one
bringing another's authorization, refused; viem's
toSimple7702SmartAccountwith the URL alone (npm), both relays. - Kernel v3.3 (2026-10-29):
@zerodev/sdk(5.5.11, the latest) has no EntryPoint v0.8, itsgetEntryPoint("0.8")v0.7's address, and Kernel v3.3's implementation is v0.7's. Its EIP-7702 mode (eip7702Account) delegates an EOA to that implementation, the client signing the authorization, ops on v0.7 witheip7702Authand no factory: it works as it is, both relays (npm). Kernel v3.3's factory and its approval vendored too. - Another's paymaster: done (2026-10-29). An op naming one pays by its fees, the EntryPoint paying the relay from the paymaster's deposit: the fee path as it is. Its limits given are kept; left out, they're measured from the op simulated with and without the paymaster, as upper bounds. Found on the way:
- The run without the paymaster isn't the op's account validation alone: the EntryPoint
takes the prefund from the account's deposit there, from the paymaster's with it, so the
difference leaves the paymaster step's deposit write out (Pimlico's sponsorship then
failed AA36). And the paymaster's validation warms what the call uses (Circle's, the
account's USDC), so the call costs less with it. Neither split is measurable from
simulateHandleOp's totals, so the measured limits are upper bounds: the whole validation, and all after it. Unused validation gas costs only prefund; unused postOp gas a penalty past 40,000 (v0.8). - A paymaster's own minimums are its business: Circle refuses a postOp limit under its gas charge (35,000), which an upper bound can still be below; its quickstart gives its limits.
- A paymaster refusing its signature (AA34) is invalid params,
paymasterData. Tested: Circle's v0.8 paymaster vendored from Ethereum (its proxy and implementation, Chainlink's ETH / USD feed, the storage they read, found byeth_createAccessList, and its deposit), its quickstart with the relay as the bundler (npm), an EOA holding only USDC; and Pimlico's singleton paymaster, its signer the tests' own, sponsoring an op (no postOp), its limits measured; a sponsorship another key signed refused. Both relays. On Sepolia (Glamsterdam, 2026-10-29,make sepolia-userops): viem's EIP-7702 account paying by its fees with the relay's URL alone, and Circle's paymaster from an EOA holding only USDC, both through the Worker. Found there, none of it on anvil: - EIP-8037 draws state gas from a reservoir, the gas a transaction has beyond EIP-7825's
execution cap (2^24), which
gasleft()doesn't count. A node runs a call without a gas limit at its own cap (tens of millions), so new state went unmeasured in simulations, by 97,920 gas a storage slot (Circle's op then failed AA26 when sent). Simulations now run at 2^24: no reservoir, as in the relay's transaction. - viem's fees there are its node's, a few hundred wei a gas against the relay's price of a
gwei, so the base fee's moves of tens of wei swung what an op paid, and it was underpaid
by the time it was sent. The estimate now prices an op at the least the base fee can
fall to by the quote's last block (
fallen, besiderisen). - The EntryPoint's own work (its accounting, deposits, compensation, new state) is outside an op's limits: the estimate planned its transaction with them, the send with a node's estimate of handleOps, which was half again more, so an op was underpaid when sent. The estimate now has a node estimate the op's simulation as sized (handleOps itself would revert on the stub signature), a second round trip for an op paying by its fees.
- An op estimated again was given its last estimate's preVerificationGas, counted as validation: simulations now set it to zero (ERC-7769: its gas fields aren't read).
- Once, an op's send failed the runner's GasExhausted, which no node reproduced minutes later (the state then, unknown); it didn't recur.
- The price before signing, for an op paying by a call (8):
postpaid_quoteUserOperationon both relays (0.5.2), the token pages' scripts quoting, then signing once. - The docs, for LLMs first: llms.txt leads by situation (library, what the account holds) to a short page an approach, each with its tested script included whole.
Order¶
- Record the sponsoring paymasters' terms. (Circle's and Pimlico's pricing and charging: recorded above, 2026-10-28.)
- Write the comparison into the user docs.
- Option 3, the relay as a bundler paid by its fees, beside option 5: "Building both" above. (Option 2, a paymaster: not taken.)
- Option 5: settle its open points, then the adapter, Kernel first, on anvil.
Bundler methods beyond ERC-7769: candidates (2026-10-30)¶
Not built: no client of Bruno's calls them, and no serious users of them turned up on GitHub. Each is cheap where it fits; build one when an integration that uses it comes along.
| Method | Who calls it | What it'd take |
|---|---|---|
pimlico_sendUserOperationNow |
permissionless.js's opt-in sendUserOperationSync |
The send, then the request held until the op's transaction settles, as an x402 request is held; its receipt answered. Every op is already in a bundle of its own ("now"). |
zd_sendUserOperationSync |
ZeroDev's SDK, once zerodevapp/sdk#280 merges; its bundler side is zerodevapp/ultra-relay#98, off by default, asked for by Privy and MegaETH's customers. Both open | The same as above, under ZeroDev's name: (userOp, entryPoint, {timeoutMs?}), the receipt; not in by the timeout (10 s by default, at most 30 s), error 4 with the op's hash (EIP-7966's "accepted, not yet included"). Its shape may change before it merges. |
pimlico_getUserOperationStatus |
permissionless.js's opt-in getUserOperationStatus |
The relay's submission status mapped to Pimlico's (not_found, submitted, included, failed, …) with its transaction hash. |
eth_getUserOperationByHash |
viem's getUserOperation (ERC-7769's own) |
The op rebuilt by decoding handleOps from its transaction once included (a finished submission keeps no call), from the queue while pending. |
Not candidates: boost_sendUserOperation (Alto's zero-fee op its operator pays for; only Pimlico's
mock paymaster calls it), pimlico_simulateAssetChange (no client found), ZeroDev's paymaster
methods (zd_sponsorUserOperation, zd_pm_accounts, stackup_getERC20TokenQuotes: Bruno is no
paymaster), debug_bundler_*.
Where they come from: ZeroDev's bundler, ultra-relay
(its src/rpc/methods/), is a fork of Pimlico's Alto and answers Alto's methods; its zd_
methods are answered by a router in front of it, which isn't public, so only those its SDK calls
are known. Pimlico's are in permissionless.js's actions/pimlico/. Neither permissionless.js nor
any of these methods is in Bruno's tests yet: permissionless.js comes in with Safe and Light
Account (toSafeSmartAccount, toLightSmartAccount).
Accounts and EntryPoints in use (2026-10-30)¶
From a day of UserOperationEvent logs on Ethereum and Base, counted from public nodes, and each
account's entryPoint() read on Ethereum and Sepolia; no dashboard splits ops by version.
| Chain | v0.6 | v0.7 | v0.8 | v0.9 |
|---|---|---|---|---|
| Ethereum (30,907 ops) | 10% | 70% | 20% | 0.3% |
| Base (22,831 ops, sampled) | 31% | 65% | 4% | 0.05% |
- v0.7 on Ethereum is mostly EIP-7702 EOAs: Trust Wallet's delegate, Alchemy's SMA7702, Kernel v3.3. Pimlico's v0.7 singleton paymaster pays most ops paid for.
- v0.8 is almost all viem's Simple7702, Pimlico's v0.8 paymaster the main one.
- v0.6 is Coinbase: Coinbase Smart Wallet (v1.0, v1.1) is v0.6 only, its EIP-7702 upgrade too. A v0.6 op is unpacked, its hash not EIP-712, its simulation the EntryPoint's own: a second codec, almost all of it for Coinbase's traffic, which mostly uses Coinbase's bundler.
- Safe's 4337 module 0.3.0 is v0.7, none for v0.8 or v0.9; it leads new deployments on Ethereum. Light Account is v0.7 (v2.0.0), v0.8 (v2.1.0) and v0.9 (v2.2.0). MetaMask's kit (Hybrid, Multisig, Stateless7702) is v0.7.
- v0.9 (
0x433709009B8330FDa32311DF1C2AFA402eD8D009, November 2025): a paymaster signature appended topaymasterAndDataand kept out of the hash, validity windows in block numbers (the top bit set), init code ignored once the account exists. viem takes it but defaults to v0.8; Pimlico's v0.9 paymaster is unmerged. Almost nothing uses it.
So next: the EIP-7702 delegates on v0.7 (MetaMask's Stateless7702, Alchemy's SMA7702, Trust's), the path Kernel v3.3 takes already; then Safe and Light Account v2.0.0 on v0.7; v0.9 as a small step after; v0.6 not unless Bruno goes to Base.
Done (2026-10-30): MetaMask's Stateless7702 and Alchemy's SemiModularAccount7702 v1.0.0 through
both relays, each with its own SDK and viem's bundler client. What each SDK leaves to the
script, as viem does for its own EIP-7702 account: signing the authorization (viem only stubs
it, to estimate); for MetaMask's kit, also no factory before delegation, and its telemetry off
(DO_NOT_TRACK). Alchemy's SDK needs viem 2.45 or later, past AgentKit's pin, so the user docs'
scripts moved to their own npm project on current viem. There, @zerodev/ecdsa-validator 5.4.9
fails to load: it requires tslib without depending on it (AgentKit's tree had hidden that), so
its scripts install tslib too (zerodevapp/sdk#281; drop it once fixed). Alchemy's v1.1.0, a security fix, isn't the SDK's default and
isn't tested.
Usage data: BundleBear.