EntryPoint IntelligenceERC-4337 on Avalanche C-Chain Data to 2026-10-25

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:

  1. 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.)
  2. 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 postOp pulls tokens from the account. Example: 0x0c9f5c6d48bbbf7261d1464920524feab7235362817dd6620e9602a93e225dd4 explained → (v0.6). Its UserOperationSponsored event reports mode 1, token USDt 0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7 evidence 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):

  1. Deploy if needed. If initCode is non-empty, call the factory (through a helper, SenderCreator) and check it created exactly sender. Emit AccountDeployed(userOpHash, sender, factory, paymaster).
  2. Compute the max cost = (all gas limits + preVerificationGas) × maxFeePerGas.
  3. 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 with missingFunds. In that no-paymaster case, the EntryPoint then debits the max cost from the account's deposit.
  4. Check the nonce (192-bit key + 64-bit sequence).
  5. If there is a paymaster: debit the max cost from the paymaster's deposit, then call paymaster.validatePaymasterUserOp(op, userOpHash, maxCost). It may return a context for postOp.
  6. Check validity time windows. Any failure here reverts the whole bundle with an AAxx code.

Then emit BeforeExecution(), the boundary.

Execution loop, for each op:

  1. 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.
  2. postOp, if the paymaster returned context (e.g. an ERC-20 paymaster pulls tokens now).
  3. Compute actualGasCost, refund the unused prefund to whoever paid (paymaster or account deposit), and emit UserOperationEvent(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_price per transaction. Don't add actualGasCost on 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.
  • actualGasCost is 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.