Learn
How it works
What the container is for, who the parties are, and how the money moves.
You already know how to read a normal C-Chain transaction: an EOA signs it, pays gas, and calls a contract. An EntryPoint transaction looks like that from the outside. Inside, it is a container: someone else's signed instructions, executed by someone else's smart account, paid for by possibly a third party. This primer explains what the container is for, who the parties are, how the money moves, and how to attribute what you see without over-claiming.
Every example is a real C-Chain address or transaction, mostly from UTC day 2026-10-18, the day measured in the earlier research. Each cited tx hash was looked up by hash in the transaction database while writing this.
Companion docs: Reading a transaction (one transaction, byte by byte), Versions (v0.6 / v0.7 / v0.8 / v0.9), Glossary.
1. Why the EntryPoint exists
What a plain EOA cannot do
An EOA is one secp256k1 key with a balance and a nonce. The protocol hard-codes its rules:
| Want | EOA problem |
|---|---|
| Batching ("approve + swap" atomically) | One tx = one call. Two calls = two txs, two signatures, and a window between them. |
| Gas sponsorship (an app pays your gas) | The signer must hold AVAX. A brand-new user with 0 AVAX can't do anything. |
| Paying gas in tokens (USDC instead of AVAX) | Gas is always paid in the native coin by tx.from. |
| Key rotation / recovery | The address is the key. Lose it and the account is gone; you can't swap in a new key. |
| Multisig, passkeys (P-256/WebAuthn), other schemes | Only one secp256k1 signature is valid. |
| Session keys / spending limits | Any signature from the key can do anything. |
A smart contract wallet can do all of that, because its rules are code. Before ERC-4337, though, a contract cannot start a transaction. Someone's EOA still had to send it and pay, and every wallet vendor built its own relayer.
How ERC-4337 fixed it without changing the chain
ERC-4337 ("Account Abstraction Using Alt Mempool") adds no new transaction type or consensus rule. Its abstract: users send a pseudo-transaction called a UserOperation "into a separate mempool". Bundlers package these into one ordinary transaction calling handleOps on "a special contract", the EntryPoint.
So the design has exactly two shared pieces:
- An alternative mempool of UserOperations, off chain. Bundlers gossip and simulate them. (You never see it on chain. You only see the bundles that made it in.)
- One singleton EntryPoint contract per version per chain, which every account, paymaster and bundler trusts. It validates each operation, makes sure someone has paid for it, executes it, and settles the gas.
Because it is "just a contract", the same addresses exist on every EVM chain, including C-Chain. Four versions are deployed here:
| Version | Address | Oct 18 activity |
|---|---|---|
| v0.6 | 0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789 evidence level: primary source |
732 txs / 733 ops |
| v0.7 | 0x0000000071727de22e5e9d8baf0edac6f37da032 evidence level: primary source |
600 / 600 |
| v0.8 | 0x4337084d9e255ff0702461cf8895ce9e3b5ff108 evidence level: primary source |
0 (used lightly in the past; see Versions) |
| v0.9 | 0x433709009b8330fda32311df1c2afa402ed8d009 evidence level: primary source |
64 / 64 |
The one-sentence model: An EntryPoint transaction is a bundler saying "here are N signed requests from N smart accounts; EntryPoint, please check each one is authorized and paid for, run it, and reimburse me."
2. The cast
Each role below has one real C-Chain example. Most come from the transaction walked through in detail in Reading a transaction: 0xddda920f2fd5092c727088b9916f632bd61628bfa118c6ddd5a2356827214ffe, which deploys a smart account and deposits 64.96 USDC into Relay.
| Role | What it is | Real example | How you know |
|---|---|---|---|
| User / signer | The key (EOA key, passkey, multisig owners…) that authorizes an op. It never appears as tx.from. |
0x3f98da05dc176902d47e989ecdae9a443ebbbbeb |
The op signature recovers to it; the account's validator logs it as owner. |
Smart account (sender) |
The contract wallet that holds assets and executes calls. "The user's address" in apps. | 0xcb7509e6466632d73a509ec778f88d52fa9096d2 (ZeroDev Kernel v3.0) |
sender field; topic2 of UserOperationEvent. |
| Factory / initCode | Deploys the account on its first op. initCode = factory address ‖ calldata. The account address is precomputed (CREATE2), so it can receive funds before it exists. |
0xd703aae79538628d27099b8c4f621be4ccd142d5 evidence level: primary source (ZeroDev meta factory → Kernel factory 0x6723b44a…d22f evidence level: primary source) |
AccountDeployed event; ZeroDev SDK constants. |
| Bundler | The EOA that signs the outer tx and pays native gas up front. Takes ops from the mempool. | 0x4337005db25dbad41da5692ba1188751ee5d98b6 evidence level: hint |
tx.from. |
| Beneficiary | Where the EntryPoint sends the collected gas payment at the end. Usually the bundler itself. | 0x4337005d…98b6 evidence level: hint (same) |
Last argument of handleOps. On Oct 18, 1,391 of 1,396 outers had beneficiary = submitter. |
| EntryPoint | The singleton referee and settlement contract. | 0x0000000071727de22e5e9d8baf0edac6f37da032 evidence level: primary source (v0.7) |
tx.to (or the emitter of UserOperationEvent if wrapped). |
| Paymaster | A contract with an AVAX deposit in the EntryPoint that agrees to pay an op's gas. | Pimlico v0.7 0x777777777777aec03fd955926dbf81597e66834c evidence level: primary source |
First 20 bytes of paymasterAndData; topic3 of UserOperationEvent. |
| Aggregator | Optional contract that verifies many signatures at once (e.g. BLS) via handleAggregatedOps. |
None observed. No direct handleAggregatedOps call (0x4b1d7cf5 / 0xdbed18e0) to any EntryPoint in the archive window 2025-01-01 → 2026-10-18 (block-archive query). |
Absent. Shown as a role so you know it exists. |
| Target app / protocol | The contracts the account calls. This is the actual "activity". | Relay Depository 0x4cd00e387622c35bddb9b4c962c136462338bc31; USDC 0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e evidence level: primary source |
Decoded from the account's callData; their own logs in the execution window. |
Paymasters: verifying mode vs ERC-20 mode
Pimlico's singleton paymasters run in two modes, which the first data byte selects:
- Verifying mode (0): Pimlico's server signs "I'll sponsor this op until time T". The chain charges Pimlico's deposit and nothing else. Who pays Pimlico (the app, via a subscription or invoice) is off chain. Example: the walk-through tx above.
- ERC-20 mode (1): the paymaster pays the AVAX, then in
postOppulls tokens from the account. Example:0x0c9f5c6d48bbbf7261d1464920524feab7235362817dd6620e9602a93e225dd4 explained →(v0.6). ItsUserOperationSponsoredevent reports mode 1, token USDt0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7evidence level: primary source, 13,386 base units (0.013386 USDt). The matching token Transfer goes to a configured settlement recipient, not the paymaster address (all-versions report).
On Oct 18, Pimlico emitted 735 UserOperationSponsored records: 734 verifying, 1 ERC-20. Other paymasters seen that day include 0x25ff7720cff5e7c479cad2a8b48f99561f7c8df9 evidence level: hint (175 ops, the signed-result workload), 0x5fa66dfe8a3983e55071e8c4631ab43b5f33a4ab evidence level: hint (169), 0x3f383d232344e297c1d98ea4b59217ae338b6223 evidence level: cluster (117) and 0xca944fb73fa5191969014ded9bb075381d59c7de evidence level: primary source (v0.9). Their modes and operators are not established here.
3. The lifecycle of one handleOps
handleOps(ops[], beneficiary) runs two loops. All validation finishes before any execution starts. That ordering protects the bundler: an op that can't pay is rejected before anything runs.
Validation loop, for each op (v0.7 _validatePrepayment; see source links in Versions):
- Deploy if needed. If
initCodeis non-empty, call the factory (through a helper,SenderCreator) and check it created exactlysender. EmitAccountDeployed(userOpHash, sender, factory, paymaster). - Compute the max cost = (all gas limits + preVerificationGas) × maxFeePerGas.
account.validateUserOp(op, userOpHash, missingFunds). The account checks the signature (any scheme it likes). If there is no paymaster, it tops up its EntryPoint deposit withmissingFunds. In that no-paymaster case, the EntryPoint then debits the max cost from the account's deposit.- Check the nonce (192-bit key + 64-bit sequence).
- If there is a paymaster: debit the max cost from the paymaster's deposit, then call
paymaster.validatePaymasterUserOp(op, userOpHash, maxCost). It may return acontextfor postOp. - Check validity time windows. Any failure here reverts the whole bundle with an
AAxxcode.
Then emit BeforeExecution(), the boundary.
Execution loop, for each op:
- Call the account with
callData(e.g.execute(...), which then calls the target apps). A failure here does not revert the bundle. The op is marked failed and still pays. postOp, if the paymaster returned context (e.g. an ERC-20 paymaster pulls tokens now).- Compute
actualGasCost, refund the unused prefund to whoever paid (paymaster or account deposit), and emitUserOperationEvent(userOpHash, sender, paymaster, nonce, success, actualGasCost, actualGasUsed).
Finally, compensate: send the sum of all actualGasCost to beneficiary in AVAX (_compensate, a plain value transfer with no event).
This diagram is wider than the screen: scroll sideways inside its frame.
sequenceDiagram
autonumber
participant B as Bundler EOA<br/>(tx.from)
participant EP as EntryPoint
participant F as Factory
participant A as Smart account<br/>(sender)
participant P as Paymaster
participant T as Target app(s)
participant Ben as Beneficiary
B->>EP: handleOps([op...],<br/>beneficiary)<br/>(bundler pays<br/>native gas)
Note over EP: ── validation loop<br/>(per op) ──
opt initCode non-empty
EP->>F: create account (via<br/>SenderCreator)
F-->>A: deploy (CREATE2)
EP-->>EP: emit<br/>AccountDeployed
end
EP->>A: validateUserOp(op,<br/>userOpHash,<br/>missingFunds)
A-->>EP: validationData (+<br/>deposit top-up if<br/>self-paying)
opt paymaster set
EP-->>EP: debit paymaster<br/>deposit (max cost)
EP->>P: validatePaymasterUserOp(op,<br/>userOpHash,<br/>maxCost)
P-->>EP: context,<br/>validationData
end
EP-->>EP: emit<br/>BeforeExecution
Note over EP: ── execution loop<br/>(per op) ──
EP->>A: callData (e.g.<br/>execute /<br/>executeBatch)
A->>T: approve / swap /<br/>transfer / ...
T-->>A: app logs (Transfer,<br/>Deposit, ...)
opt paymaster returned context
EP->>P: postOp(mode,<br/>context,<br/>actualGasCost,<br/>gasPrice)
end
EP-->>EP: refund unused<br/>prefund to deposit,<br/>emit<br/>UserOperationEvent
EP->>Ben: send Σ<br/>actualGasCost<br/>(AVAX, no event)Seeing the lifecycle in logs
The walk-through transaction shows the phases directly. Its logs, in order: account init (RootValidatorUpdated, OwnerRegistered) → AccountDeployed → Pimlico UserOperationSponsored → BeforeExecution → USDC Approval, USDC Transfer, Relay RelayErc20Deposit, USDC Approval(0) → UserOperationEvent. Everything before BeforeExecution is validation. Everything between BeforeExecution and an op's UserOperationEvent is that op's execution. With several ops, each op's window starts after the previous op's UserOperationEvent. Example with two ops in one outer: 0xfff9a920901823e8f6f2cca4ac9daf7d386cc95027af190cb9603c8ffef0475f explained → (v0.6, two collateral withdrawals from two different accounts).
Inner failure, outer success
The outer receipt can succeed while an op fails. 0x77b4647ea6918be8a39ac32c88745ffb7a817c241db0fe974a7035c61c6d1777 explained → (v0.7) has status 1, but its only UserOperationEvent says success = false. The account had requested a Circle CCTP depositForBurn on 0x28b5a0e9c621a5badaa536219b3a228c8168cf5d, and it didn't happen. The op still paid gas through its paymaster 0x03df76c8…103b evidence level: hint. It was the single failed op among Oct 18's 1,397. Never credit an op's requested action when success = false. Even when success = true, an account can catch a failing sub-call internally, so check the target's own logs.
4. Money flows
Two different fees exist in every EntryPoint transaction. Mixing them up is the most common analysis error.
| Layer | Who pays | Who receives | Amount | Where you see it |
|---|---|---|---|---|
| Outer receipt fee | bundler EOA | the chain (burned base fee + validator tip) | gas_used × effective_gas_price |
receipt; parquet txs |
UserOp charge (actualGasCost) |
paymaster's EntryPoint deposit, or the account's deposit if no paymaster | beneficiary (usually the bundler) | actualGasUsed × min(maxFee, maxPriority + basefee) |
UserOperationEvent data |
| Off-chain or token billing | the user, app, or sponsor | paymaster operator | anything: subscription, invoice, or an ERC-20 pull in postOp | partly on chain (ERC-20 mode Transfer), often invisible |
Why the first two differ. The bundler picks its own gas price for the outer tx. The op pays at the price it signed for, plus a preVerificationGas allowance for overhead the EntryPoint can't meter. The difference is the bundler's margin, which can be negative. In the walk-through tx, the bundler paid 0.002152 AVAX and was paid 0.002347 AVAX, a gain of 0.000194 AVAX, because the op allowed a 1.02 gwei tip and the bundler tipped only 0.555 gwei.
Across all of Oct 18 (earlier report, same outers):
| Version | Receipt fees (bundlers → chain) | Σ actualGasCost (deposits → beneficiaries) | Difference |
|---|---|---|---|
| v0.6 | 1.532096 AVAX | 1.251769 AVAX | beneficiaries got 0.280 AVAX less than bundlers paid |
| v0.7 | 0.842294 AVAX | 0.844403 AVAX | +0.0021 AVAX |
| v0.9 | 0.055555 AVAX | 0.042756 AVAX | −0.0128 AVAX |
Beneficiary is not always the submitter (5 of 1,396 differ), so read this as an aggregate shape, not a per-bundler P&L. Rules from the handoff, in plain terms:
- Count each outer fee once. For day totals, sum
gas_used × effective_gas_priceper transaction. Don't addactualGasCoston top; that would count the same gas twice from two sides. - Don't spread the fee over legs and sum. A 3-leg batch has one fee. If you report "fees by target", a transaction appears under several targets and those rows overlap. Say so.
actualGasCostis not bundler profit. Profit = actualGasCost received − receipt fee paid, and only when beneficiary = bundler.
"Sponsored" doesn't mean "free to the user." A nonzero paymaster proves only that the paymaster's deposit paid the AVAX for this op. The user may have paid in tokens (ERC-20 mode: 0x0c9f5c6d… pulled 0.013386 USDt). The app may have paid a monthly bill. The "sponsor" may even be the user's own company. The chain shows the deposit debit and nothing about the business arrangement.