EIP-3009 solved gasless USDC checkout for a single transfer. EIP-4337 solved smart-account UX for users who were willing to migrate to a contract wallet. The interesting space — and the one most merchants actually live in — sits between them: shoppers who already have a MetaMask or Rabby EOA, who want to pay in any token (not just USDC), and who don't want to move their funds to a brand-new wallet address just to buy a pair of shoes.
EIP-7702 is the mechanism that closes that gap. Since the Pectra upgrade activated it on Ethereum mainnet, an EOA can attach contract code to itself for the duration of a single transaction (or indefinitely, if you want to). That tiny primitive — temporary code on an EOA — is enough to give a regular wallet batched calls, sponsored gas via a paymaster, atomic multi-step flows, and scoped session keys, without the user changing addresses.
This post is how we use EIP-7702 in the BchainPay checkout, the delegate contract design we landed on, and the specific things that will bite you if you treat 7702 as "4337 lite". It will not bite you gently.
What EIP-7702 actually does#
A new EIP-7702 transaction (type 0x04, SetCode) carries an
authorization_list. Each entry is signed by an EOA and says: "For
the duration of this transaction, treat my account as if its code is
the contract at address X." When the transaction is mined, the EOA's
code slot is set to a 23-byte delegation pointer (0xef0100 || X).
After execution it can be cleared, kept for future transactions, or
overwritten.
The mental model that works: an EOA can now opt into being a delegated smart account. Its address, its history and its existing balances are unchanged. Its behaviour, while delegated, is whatever the delegate contract says it is.
Three properties matter for payments:
- Atomic batching. A single user signature can perform
approve(USDC, merchantSweep)andtransfer(USDC, merchantSweep)in one call frame. No more two-popup checkouts. - Sponsored gas. Because the EOA itself is now the entry point to a contract execution, an external relayer (or a 4337-style paymaster) can pay gas without owning the user's funds. The user pays in the token they hold.
- Scoped session keys. The delegate can authorise a secondary key to act on the EOA's behalf, but only for this merchant, this token, this amount cap, this expiry. The session key has no power outside that scope.
EIP-7702 does not replace EIP-3009, EIP-4337, or Permit2. It composes with all three. Most production checkouts will use 7702 + a Permit2 allowance + a sponsored relayer.
The minimum-viable delegate#
The delegate contract is the trust surface. The user is signing "please execute whatever code lives at address X as if it were me", so X has to be (a) immutable, (b) auditable in 5 minutes by a reasonable engineer, and (c) narrow.
Ours is ~180 lines of Solidity. The interface that matters for checkout:
contract MerchantDelegate {
// Replay protection: monotonic per-EOA nonce for batched ops.
mapping(address => uint256) public opNonce;
// Session keys scoped to (merchant, token, cap, expiry).
struct Session {
address merchant;
address token;
uint128 spendCap;
uint64 validUntil;
}
mapping(address => mapping(address => Session)) public sessions;
function authorizeSession(
address sessionKey,
Session calldata s,
uint256 nonce,
bytes calldata sig
) external;
function pay(
address token,
address to,
uint256 amount,
uint256 nonce,
bytes calldata sig
) external;
function batchPay(Call[] calldata calls, uint256 nonce, bytes calldata sig)
external;
}Three rules we baked in and would not relax:
- No
delegatecallto arbitrary targets. Every call the delegate makes is to a known token or to the merchant's sweep address. A generic "execute whatever the user signed" entry point is how you ship a wallet drainer by accident. - No upgradeability. The delegate is deployed once per chain and pinned by hash. Upgrading means a new delegate at a new address and a fresh user opt-in. Painful, deliberate, safe.
- Per-op
nonceis on the delegate, not the EOA. The EOA's transaction nonce only increments when the EOA itself sends a tx. When a relayer pays gas, the EOA's nonce stays put and the delegate'sopNonce[user]is what protects against replay.
The checkout flow#
Concretely, a one-click "Pay with USDC" flow against the BchainPay API looks like this:
1. Merchant POST /v1/payment_intents
→ returns { intentId, chainId, token, amount, merchantSweep,
delegateAddr, sessionKeyPub, sessionExpiry }
2. Shopper signs ONCE (EIP-712, typed data):
- SetCode authorization for delegateAddr (one-shot, this tx only)
- authorizeSession(sessionKeyPub, { merchantSweep, token,
spendCap=amount, validUntil })
3. Shopper POST /v1/payment_intents/:id/authorize
{ authorizationList, sessionAuthSig }
4. BchainPay relayer submits a single type-0x04 tx:
- delegates the EOA to MerchantDelegate
- calls authorizeSession(...) to install the session key
- calls pay(USDC, merchantSweep, amount, nonce, sessionSig)
5. Indexer fires payment_intent.succeeded webhook with txHash and
the receipt-side amount actually moved (post-fee).
From the shopper's perspective: one signature, one wallet popup, no
ETH required, no contract wallet to migrate to, and the sweep address
they paid is the same address they would have paid with a plain
transfer. The merchant's reconciliation logic is unchanged.
Why session keys, not direct signing every time#
You can use 7702 without session keys — sign each pay() call
individually with the EOA. We do this for one-off invoices. But for
two flows it's worth installing a short-lived session key:
- Multi-step checkout. Shopper picks shipping, applies a coupon,
changes quantity. Each tweak re-prices the intent. With a session
key scoped to
spendCap = max(intentAmount)and `validUntil = now- 10 min
, the relayer can submit the finalpay()` without going back to the wallet for another signature.
- 10 min
- Subscriptions. A 30-day session key with
spendCapper period lets the merchant pull recurring USDC without a wallet popup every month, while the user retains the ability to revoke at any time by callingrevokeSession(sessionKey)directly from their wallet.
The session key is a fresh ECDSA keypair the merchant generates server-side, scoped on-chain to a single token + recipient + cap + expiry. If the merchant's hot key is compromised, the blast radius is "that merchant's pending intents up to their per-user cap", not "the user's wallet". That's still serious, so we rotate session keys per merchant on a 24-hour schedule.
Replay, cross-chain and the address-poisoning problem#
Three failure modes we tripped on, in roughly the order they happen in production:
- Cross-chain replay of the SetCode authorization. EIP-7702
authorizations include a
chain_id, and cruciallychain_id = 0means any chain. Never sign achain_id = 0authorization for a merchant flow. We reject them at the API. If a wallet hands us one, we re-prompt with the explicit chain id derived frometh_chainId. - Stale authorizations. A SetCode authorization has no
validBefore. If a user signs one and the relayer sits on it for six hours, it's still valid. We bind the authorization to a short-livedintentIdby including the intent's keccak hash in theauthorizeSessionpayload'sSessionstruct (via abytes32 intentTagfield). If the intent expired, the session call reverts. - Address poisoning into the session. A common attack is to dust
the user's wallet with a transaction from an address that looks
like the merchant's sweep address (same first/last 4 hex chars).
If the front-end builds the
Session.merchantfield from "last address the user paid to", the attacker becomes the session recipient. Always derivemerchantSweepserver-side from the intent, never from the wallet's history.
Gas, paymasters and who actually pays#
The simple model: BchainPay's relayer EOA pays gas in ETH, and we deduct an equivalent USDC fee from the payment before sweeping to the merchant. That works on every chain that supports type-0x04 and needs no extra infrastructure.
Where it gets interesting is when you want a true 4337-style
paymaster, e.g. to let the merchant sponsor gas as a marketing
spend. EIP-7702 composes here cleanly: the delegate exposes a
validatePaymasterUserOp-shaped hook, and you wire it into a
standard 4337 EntryPoint. The user is still an EOA. The paymaster is
still a 4337 contract. The delegate is the bridge.
A few practical numbers from our mainnet rollout:
- A bare
transferis ~52k gas. A 7702 `delegate + authorizeSession- pay` in one tx is ~165k gas. The overhead is real but bounded.
- The 23-byte delegation pointer costs ~13k gas to set per transaction if the EOA isn't already delegated. We persist delegation across sessions for repeat customers, dropping per-checkout cost back to ~110k.
- Simulation (
eth_callwith the same payload) catches ~94% of failures before they consume gas. We refuse to broadcast any 7702 tx that doesn't simulate cleanly.
When to reach for 7702 vs the alternatives#
Use EIP-3009 when the user is paying in USDC (or another 3009 token) and you need the absolute minimum complexity. One signature, one transfer, done.
Use Permit2 + a relayer when you need an allowance you can re-use across multiple subsequent transactions, and the user is comfortable with a second on-chain step.
Use EIP-4337 when the user is happy to onboard a brand-new smart account and you want full account abstraction features (social recovery, multi-sig, plugin modules).
Use EIP-7702 when:
- The user already has an EOA and isn't going to migrate.
- You need atomic multi-step (e.g.,
approvethenswapthentransfer) inside one signature. - You want session keys without sending the user to a new address.
- You're paying in a token that doesn't implement EIP-3009.
Key takeaways#
- EIP-7702 lets a regular EOA temporarily behave like a smart account, unlocking batched calls, sponsored gas and session keys without changing the user's address.
- Treat the delegate contract as the security boundary. Keep it
minimal, non-upgradeable, and audited. No arbitrary
delegatecall. - Always bind authorizations to a specific
chain_idand to a short-lived intent tag.chain_id = 0is a footgun; reject it. - Session keys belong on the delegate, scoped to (merchant, token, cap, expiry). Rotate them on a fixed schedule.
- Compose, don't replace. Pair 7702 with EIP-3009 for USDC, with Permit2 for re-usable allowances, and with a 4337 paymaster when the merchant wants to sponsor gas.
- Budget ~110-165k gas per checkout, simulate every tx before broadcast, and persist delegation for repeat customers.