Account Abstraction
Account abstraction replaces the rigid EOA transaction model with programmable accounts that support arbitrary verification logic, gas sponsorship, batched operations, and session-based access control. ERC-4337 introduces an out-of-protocol smart account infrastructure (EntryPoint, bundlers, paymasters), while EIP-7702 adds a protocol-level mechanism for EOAs to temporarily or persistently delegate their execution to smart contract code. Together they form a complete stack: 7702 upgrades existing EOAs without migration, and 4337 provides the off-chain infrastructure for UserOperation bundling, gas abstraction, and paymaster sponsorship.
What You Probably Got Wrong
-
EIP-7702 does NOT replace ERC-4337 -- they are complementary. EIP-7702 is a protocol-level mechanism that lets EOAs point their code to an implementation contract. ERC-4337 is an off-chain infrastructure layer (bundlers, paymasters, EntryPoint) that processes UserOperations. You need both for a complete account abstraction stack: 7702 makes the EOA programmable, 4337 provides gas sponsorship and bundling.
-
Existing EOAs can upgrade in-place with EIP-7702 -- users do NOT need to deploy a new smart contract wallet and migrate assets. A single Type 0x04 transaction sets a delegation designator on the EOA, making it behave like a smart account while keeping the same address, balances, and history.
-
Paymasters are NOT free gas -- someone always pays. A verifying paymaster requires off-chain signature approval from the sponsor. An ERC-20 paymaster charges the user in tokens at a markup. A sponsoring paymaster has a deposit in the EntryPoint that drains with each sponsored operation. "Gasless" means gasless for the end user, not gasless for the system.
-
UserOperations are NOT wrapped transactions -- they are a completely different execution model. A UserOp goes from the client to a bundler (off-chain), the bundler packages multiple UserOps into a single
handleOpstransaction to the EntryPoint contract (on-chain), and the EntryPoint calls each account'svalidateUserOpthenexecute. The account contract itself is themsg.senderfor the inner calls, not the EOA signer. -
Session keys are NOT private keys you hand out -- they are scoped authorizations with explicit constraints: which contracts can be called, which functions, maximum value per call, time window, and total usage count. The session key validator module enforces these constraints on-chain. An expired or over-limit session key is rejected at validation time, not at execution time.
-
EntryPoint v0.7 changed the UserOp struct -- v0.6 and v0.7 are not compatible. v0.7 packs gas fields (verificationGasLimit + callGasLimit into a single bytes32), uses
accountGasLimitsinstead of separate fields, and addspaymasterAndDatapacking. If your bundler returns "invalid UserOp" errors, check which EntryPoint version you are targeting. -
Nonces are 2D in ERC-4337 -- the nonce field is a
uint256where the upper 192 bits are the "key" and the lower 64 bits are the sequential nonce for that key. This allows parallel UserOp submission across different nonce keys without blocking. Most SDKs handle this automatically, but raw UserOp construction requires understanding this.
ERC-4337 Core Architecture
EntryPoint v0.7
The EntryPoint is a singleton contract deployed at a deterministic address across all EVM chains. It serves as the central coordinator: validating UserOperations, calling smart account contracts, handling paymaster interactions, and managing gas accounting.
Last verified: February 2026
| Contract | Address |
|---|---|
| EntryPoint v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
| EntryPoint v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
UserOperation Struct (v0.7)
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
bytes32 accountGasLimits; // verificationGasLimit (16 bytes) || callGasLimit (16 bytes)
uint256 preVerificationGas;
bytes32 gasFees; // maxPriorityFeePerGas (16 bytes) || maxFeePerGas (16 bytes)
bytes paymasterAndData; // paymaster (20 bytes) || paymasterVerificationGasLimit (16 bytes) || paymasterPostOpGasLimit (16 bytes) || paymasterData
bytes signature;
}
| Field | Purpose |
|---|---|
sender |
Smart account address that will execute the operation |
nonce |
Anti-replay value (upper 192 bits = key, lower 64 bits = sequence) |
initCode |
Factory address + calldata for first-time account deployment (empty if account exists) |
callData |
Encoded call to the account's execute function |
accountGasLimits |
Packed verification and call gas limits |
preVerificationGas |
Gas to compensate bundler for calldata and overhead costs |
gasFees |
Packed EIP-1559 gas price fields |
paymasterAndData |
Paymaster address + gas limits + custom data (empty if self-paying) |
signature |
Signature validated by the account's validateUserOp |
Smart Account Interface
Every ERC-4337 smart account must implement:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IAccount {
/// @notice Validates a UserOperation
/// @param userOp The packed user operation
/// @param userOpHash Hash of the user operation (used for signature verification)
/// @param missingAccountFunds Amount the account must prefund the EntryPoint
/// @return validationData 0 for success, 1 for failure, or packed (authorizer, validUntil, validAfter)
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData);
}
UserOperation Lifecycle
1. Client constructs UserOp (callData, gas limits, signature)
|
2. Client sends UserOp to bundler via eth_sendUserOperation RPC
|
3. Bundler validates UserOp locally (simulation, gas checks, signature)
|
4. Bundler packages UserOps into a bundle transaction
|
5. Bundler calls EntryPoint.handleOps(ops[], beneficiary)
|
6. EntryPoint loops through each UserOp:
a. If initCode present -> deploy account via factory
b. Call account.validateUserOp() -> must return 0 for success
c. If paymaster present -> call paymaster.validatePaymasterUserOp()
d. Call account with callData (the actual operation)
e. If paymaster present -> call paymaster.postOp()
|
7. EntryPoint settles gas: refunds excess, pays bundler from account/paymaster deposit
Validation Phase
The validation phase runs with a restricted opcode set (no TIMESTAMP, BLOCKHASH, etc. relative to other accounts) to ensure simulation determinism. The account's validateUserOp must:
- Verify the signature over
userOpHash - Pay
missingAccountFundsto the EntryPoint (or have sufficient deposit) - Return
0for valid,1for invalid, or a packed value with time-range validity
Execution Phase
After all UserOps in a bundle pass validation, the EntryPoint calls each account with the callData. The account contract is msg.sender for any downstream calls. If execution reverts, the gas is still consumed and paid for.
Bundler Role
Bundlers are off-chain services that collect UserOperations from users, validate them, and submit them as bundle transactions to the EntryPoint. They earn gas refunds as compensation.
Bundler RPC Methods
| Method | Purpose |
|---|---|
eth_sendUserOperation |
Submit a UserOp for inclusion |
eth_estimateUserOperationGas |
Estimate gas limits for a UserOp |
eth_getUserOperationByHash |
Look up a UserOp by its hash |
eth_getUserOperationReceipt |
Get execution receipt for a UserOp |
eth_supportedEntryPoints |
List EntryPoint addresses the bundler supports |
eth_chainId |
Return the chain ID |
Major Bundler Providers
| Provider | Endpoint Format | Notes |
|---|---|---|
| Pimlico | https://api.pimlico.io/v2/{chain}/rpc?apikey={key} |
Alto bundler, widest chain support |
| Alchemy | https://{chain}.g.alchemy.com/v2/{key} |
Rundler, integrated with Alchemy platform |
| Stackup | https://api.stackup.sh/v1/node/{key} |
Stackup bundler, ERC-4337 pioneers |
EIP-7702 EOA Delegation
EIP-7702 introduces Type 0x04 transactions that let EOAs set a delegation designator, making the EOA's code point to a smart contract implementation. This is a protocol-level change (Pectra upgrade) that does not require ERC-4337 infrastructure.
How Delegation Works
A Type 0x04 transaction includes an authorizationList -- an array of signed authorization tuples. When processed, the EOA's code is set to 0xef0100 || address, a special delegation designator prefix. Any call to the EOA now delegates execution to the implementation contract at address.
Authorization Tuple
interface Authorization {
chainId: bigint; // 0 for chain-agnostic, or specific chain ID
address: Address; // Implementation contract to delegate to
nonce: bigint; // EOA's current nonce (prevents replay)
yParity: number; // Signature recovery parameter
r: `0x${string}`; // Signature component
s: `0x${string}`; // Signature component
}
Delegation with viem
import { createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { eip7702Actions } from "viem/experimental";
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
}).extend(eip7702Actions());
// BatchExecutor is a contract that implements execute(Call[]) for batching
const BATCH_EXECUTOR = "0x..." as const; // Your batch executor implementation
async function delegateAndBatch() {
const authorization = await walletClient.signAuthorization({
contractAddress: BATCH_EXECUTOR,
});
const hash = await walletClient.sendTransaction({
authorizationList: [authorization],
to: account.address,
data: encodeFunctionData({
abi: batchExecutorAbi,
functionName: "execute",
args: [
[
{ target: TOKEN_A, value: 0n, data: approveCalldata },
{ target: ROUTER, value: 0n, data: swapCalldata },
],
],
}),
});
return hash;
}
Per-Transaction vs Persistent Delegation
- Per-transaction: Sign an authorization with
chainId: 0or the specific chain. The delegation persists until explicitly cleared or overwritten by another Type 0x04 transaction. There is no automatic expiry. - Clearing delegation: Send a Type 0x04 transaction with an authorization pointing to
address(0)to remove the delegation designator and restore the EOA to its original state. - Key insight: The EOA retains its private key and can always send a new Type 0x04 transaction to change or clear delegation. The delegated code cannot lock out the EOA owner.
ERC-4337 + EIP-7702 Combined
The most powerful pattern combines both standards: EIP-7702 makes the EOA behave like a smart account, and ERC-4337 provides bundler infrastructure and paymaster sponsorship for that account.
How It Works
- User signs an EIP-7702 authorization delegating their EOA to a smart account implementation (e.g., SimpleAccount, Kernel)
- The UserOp includes the
eip7702Authin its authorization list - The bundler submits a Type 0x04 transaction that both sets the delegation and processes the UserOp through the EntryPoint
- The EOA now has smart account capabilities (validation logic, execute function) and can use paymasters
import { toSmartAccount } from "permissionless/accounts";
import { createSmartAccountClient } from "permissionless";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { http } from "viem";
import { mainnet } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
import { eip7702Actions } from "viem/experimental";
const owner = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const pimlicoClient = createPimlicoClient({
transport: http(
`https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
),
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
// The EOA becomes the smart account via 7702 delegation
const smartAccount = await toSmartAccount({
client: publicClient,
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
owner,
// 7702 delegation target -- the EOA will delegate to this implementation
implementation: SMART_ACCOUNT_IMPLEMENTATION,
});
const smartAccountClient = createSmartAccountClient({
account: smartAccount,
chain: mainnet,
bundlerTransport: http(
`https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
),
paymaster: pimlicoClient,
});
Benefits of Combined Approach
| Capability | 4337 Only | 7702 Only | Combined |
|---|---|---|---|
| Same EOA address | No (new contract) | Yes | Yes |
| Gas sponsorship | Yes (paymasters) | No | Yes |
| Batched calls | Yes | Yes | Yes |
| Session keys | Yes (modules) | Requires custom impl | Yes (modules) |
| Works without bundler | No | Yes | Flexible |
| Cross-chain replay protection | Via nonce keys | Via chainId in auth | Both |
Paymaster Patterns
Verifying Paymaster
A verifying paymaster sponsors gas for UserOps that carry a valid off-chain signature from the paymaster operator. The operator controls who gets sponsored.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
/// @notice Sponsors UserOps that carry a valid signature from the verifying signer
contract VerifyingPaymaster is IPaymaster {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
address public immutable entryPoint;
address public verifyingSigner;
error InvalidSignature();
error CallerNotEntryPoint();
modifier onlyEntryPoint() {
if (msg.sender != entryPoint) revert CallerNotEntryPoint();
_;
}
constructor(address _entryPoint, address _signer) {
entryPoint = _entryPoint;
verifyingSigner = _signer;
}
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external onlyEntryPoint returns (bytes memory context, uint256 validationData) {
// paymasterData layout: signature (65 bytes)
bytes calldata paymasterData = userOp.paymasterAndData[52:];
bytes32 hash = keccak256(
abi.encode(userOp.sender, userOp.nonce, userOpHash)
).toEthSignedMessageHash();
address recovered = hash.recover(paymasterData[:65]);
if (recovered != verifyingSigner) revert InvalidSignature();
return (abi.encode(userOp.sender), 0);
}
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost,
uint256 actualUserOpFeePerGas
) external onlyEntryPoint {}
}
ERC-20 Paymaster
An ERC-20 paymaster lets users pay gas fees in ERC-20 tokens instead of ETH. The paymaster holds an ETH deposit in the EntryPoint and charges the user's tokens in postOp.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
/// @notice Charges users in ERC-20 tokens for gas sponsorship
/// @dev Users must approve this paymaster for the payment token
contract ERC20Paymaster is IPaymaster {
address public immutable entryPoint;
IERC20 public immutable token;
// Price oracle would set this -- simplified for reference
uint256 public tokenPricePerGas;
error CallerNotEntryPoint();
error InsufficientTokenBalance();
modifier onlyEntryPoint() {
if (msg.sender != entryPoint) revert CallerNotEntryPoint();
_;
}
constructor(address _entryPoint, address _token, uint256 _tokenPricePerGas) {
entryPoint = _entryPoint;
token = IERC20(_token);
tokenPricePerGas = _tokenPricePerGas;
}
function validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32,
uint256 maxCost
) external onlyEntryPoint returns (bytes memory context, uint256 validationData) {
uint256 maxTokenCost = maxCost * tokenPricePerGas;
uint256 balance = token.balanceOf(userOp.sender);
if (balance < maxTokenCost) revert InsufficientTokenBalance();
return (abi.encode(userOp.sender, maxTokenCost), 0);
}
function postOp(
PostOpMode,
bytes calldata context,
uint256 actualGasCost,
uint256
) external onlyEntryPoint {
(address sender, ) = abi.decode(context, (address, uint256));
uint256 actualTokenCost = actualGasCost * tokenPricePerGas;
// CEI: state is updated via token transfer, then no further external calls
token.transferFrom(sender, address(this), actualTokenCost);
}
}
Client-Side Paymaster Integration
import { createSmartAccountClient } from "permissionless";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { http } from "viem";
import { entryPoint07Address } from "viem/account-abstraction";
const pimlicoClient = createPimlicoClient({
transport: http(
`https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
),
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
const smartAccountClient = createSmartAccountClient({
account: smartAccount,
chain: mainnet,
bundlerTransport: http(
`https://api.pimlico.io/v2/1/rpc?apikey=${process.env.PIMLICO_API_KEY}`
),
paymaster: pimlicoClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast;
},
},
});
const txHash = await smartAccountClient.sendTransaction({
to: "0xRecipient..." as `0x${string}`,
value: parseEther("0.01"),
data: "0x",
});
SDK: permissionless.js
permissionless.js is a TypeScript SDK built on viem for ERC-4337 smart accounts. It supports multiple smart account implementations, bundlers, and paymasters.
Setup
npm install permissionless viem
Create Smart Account Client
import { createSmartAccountClient } from "permissionless";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
const publicClient = createPublicClient({
chain: sepolia,
transport: http(process.env.RPC_URL),
});
const owner = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const simpleAccount = await toSimpleSmartAccount({
client: publicClient,
owner,
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
const smartAccountClient = createSmartAccountClient({
account: simpleAccount,
chain: sepolia,
bundlerTransport: http(
`https://api.pimlico.io/v2/sepolia/rpc?apikey=${process.env.PIMLICO_API_KEY}`
),
});
Send a UserOp
const txHash = await smartAccountClient.sendTransaction({
to: "0xRecipient..." as `0x${string}`,
value: parseEther("0.01"),
data: "0x",
});
console.log(`Transaction hash: ${txHash}`);
Batch Transactions
const txHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "approve",
args: [SPENDER, parseEther("100")],
},
{
to: PROTOCOL_ADDRESS,
abi: protocolAbi,
functionName: "deposit",
args: [parseEther("100")],
},
],
});
const receipt = await smartAccountClient.waitForUserOperationReceipt({
hash: txHash,
});
if (!receipt.success) {
throw new Error(`UserOp failed: ${receipt.reason}`);
}
SDK: ZeroDev
ZeroDev provides the Kernel smart account -- a modular ERC-7579 compatible smart account with plugin support for session keys, passkeys, and recovery.
Kernel Account Setup
npm install @zerodev/sdk @zerodev/ecdsa-validator permissionless viem
import { createKernelAccount, createKernelAccountClient } from "@zerodev/sdk";
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
const publicClient = createPublicClient({
chain: sepolia,
transport: http(process.env.RPC_URL),
});
const signer = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
signer,
entryPoint: entryPoint07Address,
});
const kernelAccount = await createKernelAccount(publicClient, {
plugins: {
sudo: ecdsaValidator,
},
entryPoint: entryPoint07Address,
});
const kernelClient = createKernelAccountClient({
account: kernelAccount,
chain: sepolia,
bundlerTransport: http(process.env.BUNDLER_URL),
paymaster: {
getPaymasterData: async (userOperation) => {
// Return paymaster data from your paymaster service
return { paymasterAndData: "0x" };
},
},
});
Send Transaction with Kernel
const txHash = await kernelClient.sendTransaction({
to: "0xRecipient..." as `0x${string}`,
value: parseEther("0.01"),
data: "0x",
});
Session Keys
Session keys are scoped signing keys that allow limited actions without requiring the main account owner's signature for every operation. They are implemented as ERC-7579 validator modules.
Session Key Constraints
| Constraint | Description |
|---|---|
| Contract whitelist | Only specific contract addresses can be called |
| Function whitelist | Only specific function selectors are allowed |
| Value limit | Maximum ETH value per call |
| Time window | Valid only between validAfter and validUntil timestamps |
| Usage count | Maximum number of times the session key can be used |
| Spending limit | Maximum total token spend across all calls |
Creating a Session Key with ZeroDev
import {
createKernelAccount,
createKernelAccountClient,
} from "@zerodev/sdk";
import {
toPermissionValidator,
toSudoPolicy,
toCallPolicy,
CallPolicyVersion,
ParamCondition,
} from "@zerodev/permissions";
import { toECDSASigner } from "@zerodev/permissions/signers";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
// Generate a temporary session key
const sessionPrivateKey = generatePrivateKey();
const sessionKeySigner = privateKeyToAccount(sessionPrivateKey);
const ecdsaSigner = toECDSASigner({
signer: sessionKeySigner,
});
const permissionValidator = await toPermissionValidator(publicClient, {
entryPoint: entryPoint07Address,
signer: ecdsaSigner,
policies: [
toCallPolicy({
policyVersion: CallPolicyVersion.V0_0_4,
permissions: [
{
target: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "transfer",
args: [
{
condition: ParamCondition.EQUAL,
value: ALLOWED_RECIPIENT,
},
null, // Any amount (or add a condition)
],
},
],
}),
],
});
const sessionKeyAccount = await createKernelAccount(publicClient, {
plugins: {
sudo: ecdsaValidator,
regular: permissionValidator,
},
entryPoint: entryPoint07Address,
});
Using a Session Key
const sessionClient = createKernelAccountClient({
account: sessionKeyAccount,
chain: sepolia,
bundlerTransport: http(process.env.BUNDLER_URL),
});
// This transaction is signed by the session key, validated by the permission module
const txHash = await sessionClient.sendTransaction({
to: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "transfer",
args: [ALLOWED_RECIPIENT, parseEther("10")],
});
Revoking a Session Key
Session keys can be revoked by the account owner by disabling the permission validator or removing the specific session from the validator's storage:
const revokeTx = await kernelClient.sendUserOperation({
calls: [
{
to: kernelAccount.address,
data: encodeFunctionData({
abi: kernelAccountAbi,
functionName: "disablePlugin",
args: [permissionValidatorAddress],
}),
},
],
});
Gas Abstraction in Practice
To build a fully gasless flow, create a smartAccountClient with a paymaster parameter. The paymaster's EntryPoint deposit covers all gas costs. See the examples/simple-smart-account/ example for a complete working implementation.
Gas Estimation for UserOps
const gasEstimate = await pimlicoClient.estimateUserOperationGas({
sender: account.address,
nonce: await account.getNonce(),
callData: await account.encodeCalls([
{ to: recipient, value: parseEther("0.01"), data: "0x" },
]),
signature: await account.getDummySignature(),
});
// gasEstimate contains:
// - preVerificationGas: bundler overhead compensation
// - verificationGasLimit: gas for validateUserOp
// - callGasLimit: gas for the actual execution
preVerificationGas Calculation
preVerificationGas compensates the bundler for calldata costs and fixed overhead. On L2s with L1 data posting costs (Arbitrum, Optimism, Base), this value is significantly higher because it includes the L1 data fee.
// L1 chains: ~21000 + calldata_cost
// L2 chains: ~21000 + calldata_cost + l1_data_fee
// Always use bundler estimation -- manual calculation is error-prone on L2s
const gasPrice = await pimlicoClient.getUserOperationGasPrice();
Choosing Your Stack
| Use Case | Recommendation | Why |
|---|---|---|
| New app, new users, gasless onboarding | ERC-4337 + permissionless.js + Pimlico | Full gas abstraction, paymaster sponsoring, widest tooling |
| Existing app, users have EOAs | EIP-7702 only | No migration needed, no new addresses, works with existing wallets |
| DeFi power users wanting batching | EIP-7702 only | Minimal overhead, batch calls in single tx, keep same address |
| Gaming with session-based actions | ERC-4337 + ZeroDev Kernel | Session keys for in-game actions, modular validators |
| Enterprise with compliance needs | ERC-4337 + Safe | Multisig, role-based access, audit trail |
| Maximum flexibility | ERC-4337 + EIP-7702 combined | EOA upgrades in-place, gets paymaster + session key support |
| Mobile wallet | ERC-4337 + passkey signer | WebAuthn/passkey as account signer, no seed phrase |
When to Use 4337 Only
- You need paymaster gas sponsorship
- You need modular validator/executor plugins (ERC-7579)
- Your users are creating new accounts (onboarding flow)
- You need bundler infrastructure (high throughput, MEV protection)
When to Use 7702 Only
- Your users already have EOAs with assets
- You need batching but not gas sponsorship
- You want minimal infrastructure overhead
- You are building a wallet upgrade feature
When to Combine
- You want EOAs to get paymaster sponsorship
- You need session keys on existing EOA addresses
- You want the "best of both worlds" without address migration
Contract Addresses
Last verified: February 2026. EntryPoint is deployed at deterministic addresses via CREATE2 across all EVM chains.
| Contract | Address | Chains |
|---|---|---|
| EntryPoint v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
All EVM chains |
| EntryPoint v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
All EVM chains |
| SimpleAccountFactory (v0.7) | 0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985 |
Ethereum, Arbitrum, Base, Optimism, Polygon |
# Verify EntryPoint deployment on any chain
cast code 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --rpc-url $RPC_URL