Bundler is the central infrastructure component of the ERC-4337 ecosystem. We create production-ready bundlers for your tasks: from a basic centralized version to an MEV-optimized one with a P2P mempool. Our experience: over 5 years in blockchain development and over 50 ERC-4337 solution deployments. It is the bundler that makes Account Abstraction work: it receives UserOperations from users, validates them, batches them, and sends them on-chain via EntryPoint.handleOps(). Without a bundler, Account Abstraction does not work—there is no mechanism to deliver UserOps to the blockchain.
Developing your own bundler is relevant when: you need custom mempool logic, MEV optimization for UserOps, a private bundler for a specific application, or when you need to understand and control the entire ERC-4337 infrastructure. We guarantee correct implementation of storage access rules—the key complexity that trips up 70% of homemade bundlers.
How a Bundler Validates UserOperations
A user creates a UserOperation and sends it to the bundler via the JSON-RPC method eth_sendUserOperation. The bundler performs a series of checks, keeps the UserOp in its alt mempool, and periodically submits batches on-chain.
Phase 1: Validation
The bundler calls EntryPoint.simulateValidation(userOp). This is a view function (reverts with a result via a custom error) that:
- If
initCodeis not empty—deploys the Account contract via a factory. - Calls
account.validateUserOp()—checks signature, nonce. - If a Paymaster is specified—calls
paymaster.validatePaymasterUserOp(). - Returns a
ValidationResultwith gas data, paymaster staking info, and time constraints.
interface ValidationResult {
returnInfo: {
preOpGas: bigint;
prefund: bigint; // how much ETH account/paymaster deposited in EntryPoint
sigFailed: boolean;
validAfter: number;
validUntil: number;
};
senderInfo: StakeInfo;
factoryInfo?: StakeInfo;
paymasterInfo?: StakeInfo;
}
prefund is key. The account or paymaster must have a deposit in the EntryPoint sufficient to cover maxFeePerGas * (verificationGasLimit + callGasLimit). The bundler checks this before including in the mempool.
Why Storage Access Rules Are Critical
ERC-4337 imposes strict restrictions on what storage validateUserOp can read/write. The goal is to prevent a situation where one UserOp invalidates others (griefing attack).
Prohibited during validation:
- Read storage of other contracts except the account itself and related entities.
- Call
block.timestamp,block.number(except limited use viavalidAfter/validUntil). - Access storage that might be changed by another UserOp in the same batch.
This requirement is described in the ERC-4337 specification. The bundler tracks the storage slots accessed during validation using debug_traceCall with an EVM tracer. This is a costly operation—one of the main performance bottlenecks of a bundler.
// Simplified tracer for tracking storage access
async function traceValidation(userOp: UserOperation): Promise<StorageMap> {
const trace = await provider.send('debug_traceCall', [{
to: ENTRY_POINT_ADDRESS,
data: entryPoint.interface.encodeFunctionData('simulateValidation', [userOp])
}, 'latest', {
tracer: bundlerCollectorTracer, // custom JS tracer
tracerConfig: { /* ... */ }
}])
return parseStorageAccess(trace)
}
bundlerCollectorTracer is a JavaScript tracer for go-ethereum's debug_traceCall. It tracks every SLOAD/SSTORE opcode and associates them with the calling contract. This is the most technically challenging part of a bundler.
Managing the Alternative Mempool
A UserOp accepted into the mempool must remain valid. The bundler monitors:
- Nonce invalidation. If the on-chain nonce of an account changes (another UserOp went through), the pending UserOp with the old nonce is removed.
- Deposit insufficiency. If the deposit balance in the EntryPoint decreases (another UserOp sponsored by the same Paymaster went through), the bundler must recalculate whether it covers all pending UserOps of that Paymaster.
- Gas price changes. A UserOp with
maxFeePerGasbelow the current base fee will not pass; the bundler may temporarily defer or drop it.
class UserOpMempool {
private pool: Map<string, MempoolEntry> = new Map()
async add(userOp: UserOperation): Promise<string> {
const hash = getUserOpHash(userOp)
// Reputation system: limit by sender/paymaster/factory
this.reputationManager.checkReputation(userOp)
this.pool.set(hash, {
userOp,
prefund: await this.calculatePrefund(userOp),
addedAt: Date.now()
})
return hash
}
getBundle(maxGas: bigint): UserOperation[] {
// Greedy algorithm: select UserOps with highest priority fee
// taking into account gas limit and storage conflicts
return this.selectNonConflicting(
[...this.pool.values()]
.sort((a, b) => Number(b.userOp.maxPriorityFeePerGas - a.userOp.maxPriorityFeePerGas)),
maxGas
)
}
}
Submitting a Bundle On-chain
The bundler forms a batch of valid UserOps and sends EntryPoint.handleOps(ops, beneficiary). beneficiary is the address where the EntryPoint will send the collected gas (priority fee of the bundler).
Critical point: the bundler sends a regular EOA transaction. It pays gas upfront; the EntryPoint reimburses from the deposits of accounts/paymasters. If handleOps reverts, the bundler loses gas. Therefore, simulation before submission is mandatory.
Protection against reverting bundles: In handleOps, the EntryPoint skips UserOps that revert during the execution phase (not validation). For the validation phase—if it reverts, the entire handleOps fails. The bundler must ensure validation is guaranteed to pass.
Decentralization and Protection: Reputation and P2P
To prevent spam and DoS attacks, ERC-4337 introduces a reputation system for unbanned entities (Paymaster, Factory, Aggregator). The logic:
class ReputationManager {
// For each entity track: ops included vs ops unsuccessful
updateIncluded(entity: string): void {
this.entries[entity].opsSeen++
this.entries[entity].opsIncluded++
}
updateFailed(entity: string): void {
this.entries[entity].opsIncluded-- // if bundle was reverted
}
getStatus(entity: string): 'ok' | 'throttled' | 'banned' {
const entry = this.entries[entity]
if (!entry) return 'ok'
const ratio = entry.opsIncluded / Math.max(1, entry.opsSeen)
if (ratio < MIN_INCLUSION_RATE_DENOMINATOR) return 'banned'
if (entry.opsSeen > THROTTLE_THRESHOLD) return 'throttled'
return 'ok'
}
}
Staking in the EntryPoint increases limits: an entity with stake can have more UserOps in the mempool. This is an anti-spam mechanism: you cannot freely spam the mempool.
For a decentralized bundler, a P2P alt mempool is needed—a network for exchanging UserOps between bundler nodes. ERC-4337 specifies a protocol based on libp2p with gossipsub:
- Topic:
user_ops/{chainId}/{entryPointAddress} - Message: RLP-encoded UserOperation
- Validation: each node independently validates before relay
import { createLibp2p } from 'libp2p'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
const libp2p = await createLibp2p({
/* ... transport, identify, etc */
services: {
pubsub: gossipsub({
allowPublishToZeroPeers: true,
msgIdFn: (msg) => computeUserOpHash(msg.data)
})
}
})
libp2p.services.pubsub.subscribe(userOpsTopic)
libp2p.services.pubsub.addEventListener('message', async (event) => {
const userOp = decodeUserOp(event.detail.data)
await mempool.add(userOp) // with all checks
})
MEV and Bundle Construction
A bundler has a unique position: it selects the order of UserOps in a bundle, which opens up MEV opportunities. Two strategies:
Fair FIFO bundler—includes UserOps in the order received, maximizes priority fee. Simple implementation, good for a permissioned bundler for a specific application.
MEV-aware bundler—analyzes the callData of UserOps, finds arbitrage opportunities, and builds the bundle optimally. Integration with Flashbots MEV-boost to submit bundles via a private mempool. Saves 15-25% on gas costs with MEV optimization.
| Strategy | Advantages | Disadvantages |
|---|---|---|
| FIFO | Simplicity, predictability | Misses MEV |
| MEV-aware | Additional revenue | More complex, requires callData analysis |
What's Included in the Work
- Documentation—architecture description, storage access rules, interaction scheme.
- Access—to the repository, GitHub Actions, monitoring.
- Training—workshop on operation and configuration of the bundler.
- Support—2 months after launch, including critical bug fixes.
- Source code—under a license, with deployment instructions.
Ready-made Implementations for Forking
- Infinitism/bundler (TypeScript)—reference implementation from the creators of ERC-4337
- Stackup bundler (Go)—production bundler from Stackup
- Silius (Rust)—high-performance bundler
- Rundler (Rust)—bundler from Alchemy
For custom development: TypeScript reference is easier to understand; Rust/Go are better for production throughput.
Technology Stack and Timelines
| Component | Technology |
|---|---|
| RPC server | Node.js / Go / Rust |
| EVM tracing | debug_traceCall + custom JS tracer |
| Mempool storage | Redis / in-memory + persistence |
| P2P (optional) | libp2p + gossipsub |
| Monitoring | Prometheus + Grafana |
| Testing | Foundry + Hardhat (local EntryPoint) |
Basic centralized bundler with RPC, validation, mempool, and bundle submission: 6-8 weeks. The main difficulty is a correct EVM tracer for storage access rules.
Production bundler with reputation system, P2P mempool, MEV optimization, monitoring: 3-4 months.
Key warning: incorrect implementation of storage access rules leads either to accepting dangerous UserOps (DoS risk) or rejecting valid ones (poor UX). Thorough testing on all edge cases is mandatory.
Order a custom bundler development—we will implement it for your infrastructure. Contact us for a consultation: we will help choose a strategy and calculate the cost for your task.







