Skip to content

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 a handleOps call to the EntryPoint, a singleton contract. The EntryPoint pays the bundler's chosen beneficiary all 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 its postOp after the execution. In postOp's opReverted mode, "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 the 0x7702 factory 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.

Sources: EIP-5792, ERC-7677.

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. paymasterAndData can 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's updatedAt isn'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, plus additionalGasCharge gas at maxFeePerGas, at the oracle's price, plus the spread. In postOp it charges the actual gas cost the same way, and adds 10% of the unused postOp gas 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. postOp ignores 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, feeSpread was 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 is additionalGasCharge: 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.
  • validUntil and validAfter: 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's maxCost at the rate), constantFee, a flat fee on top, and recipient.
  • 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 a recipient set, a prefund above the cost goes to the recipient, not back to the account.
  • Reverts. postOp ignores its mode here too: a reverted UserOp is charged.
  • Approval. The account approves the paymaster beforehand, for at least the UserOp's maxCost in 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 upfront flow doesn't.

Ways Bruno could meet the ecosystem

Each is open. For each, what it would take is to research.

  1. Stay outside, as now, and explain the difference in the docs for someone choosing between them.
  2. Bruno as an ERC-7677 paymaster service. Any wallet that supports paymasterService could pay through Bruno with no SDK of ours. It needs a paymaster contract, a deposit in each EntryPoint version, and a charge in postOp. 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.
  3. Bruno as a bundler (eth_sendUserOperation). Bruno would take UserOps and send handleOps through 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.
  4. 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.
  5. 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's executeBatch and 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 handleOps call, 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's custom(), ZeroDev's bundlerTransport and 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, a prepareUserOperation that viem's and ZeroDev's clients look up on the client (viem 2.38's getAction) adds it for them, typed by the shape it needs and tested against the versions npm-test pins.
  • 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 by eth_call's state override, runs simulateHandleOp with the account's stub signature and reports the signature failed (in validationData) 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; its deposits mapping, slot 0), paid is 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: preOpGas 293,361, execution 39,467; limits 322,697 and 43,413; the signed op's handleOps estimated 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. EntryPointSimulations is never deployed: its code is eth-infinitism's published build (@account-abstraction/contracts 0.7.0), whose EntryPoint matches Ethereum's byte for byte but the SenderCreator immutable. One eth_call with 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 handleOps call 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_estimateUserOperationGas itself, on the chain's JSON-RPC endpoint and at POST /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_supportedEntryPoints and eth_chainId: the relay's URL is a bundler transport. viem's own createBundlerClient, 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's createKernelAccountClient, the relay its bundlerTransport, 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 (in paymasterAndData) 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:

  1. The gas price methods choose before there's an op. zd_getUserOperationGasPrice (which ZeroDev's client calls unless given estimateFeesPerGas) and pimlico_getUserOperationGasPrice take 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.
  2. 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.
  3. preVerificationGas is 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.
  4. 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.
  5. A failed op: paid for when its fees pay (handleOps compensates 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.
  6. 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.
  7. 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.
  8. 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 paymaster whose getPaymasterData signs an EIP-2612 permit to it, fees from pimlico_getUserOperationGasPrice on the bundler, Pimlico's public bundler): changes only the bundler's URL. For that, the relay answers pimlico_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 sendUserOperation differs in three things: fees zero, a last call paying the payee, at the relay's price. As npm-userops/userop-*.ts does 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

  1. 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 preVerificationGas the rest of the transaction's gas (planned as any call's, less what the op uses); pimlico_getUserOperationGasPrice and zd_getUserOperationGasPrice answer a quote's price a gas as both fees, a flat price to EntryPoint v0.7 whatever the base fee. Found on the way:
  2. 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 preVerificationGas is 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 asks zd_getUserOperationGasPrice by default.
  3. An op whose price isn't flat pays by the base fee of the block it lands in, while a node runs eth_call at a base fee of 0. So the runner checks what its fees pay at any base fee (its gas at min(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.
  4. EntryPointSimulations charged an op 0.4% more gas than the EntryPoint did (458,215 against 456,370), so a payment read from its paid failed the runner's check. A send now runs the op through the EntryPoint's own handleOps, 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.
  5. 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.
  6. 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 in eth_supportedEntryPoints, v0.8 first. Found on the way:
  7. 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.
  8. 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.
  9. 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 toSimple7702SmartAccount with the URL alone (npm), both relays.
  10. Kernel v3.3 (2026-10-29): @zerodev/sdk (5.5.11, the latest) has no EntryPoint v0.8, its getEntryPoint("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 with eip7702Auth and no factory: it works as it is, both relays (npm). Kernel v3.3's factory and its approval vendored too.
  11. 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:
  12. 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).
  13. 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.
  14. 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 by eth_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:
  15. 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.
  16. 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, beside risen).
  17. 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.
  18. 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).
  19. Once, an op's send failed the runner's GasExhausted, which no node reproduced minutes later (the state then, unknown); it didn't recur.
  20. The price before signing, for an op paying by a call (8): postpaid_quoteUserOperation on both relays (0.5.2), the token pages' scripts quoting, then signing once.
  21. 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

  1. Record the sponsoring paymasters' terms. (Circle's and Pimlico's pricing and charging: recorded above, 2026-10-28.)
  2. Write the comparison into the user docs.
  3. Option 3, the relay as a bundler paid by its fees, beside option 5: "Building both" above. (Option 2, a paymaster: not taken.)
  4. 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 to paymasterAndData and 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.