Deterministic gas & reputation optimization strategies for ERC-4337 bundlers, with mempool simulation rules.
Bundlers are the unsung bottleneck of Account Abstraction. Most bundlers today use naive fee-ordering or basic priority scoring. This repo implements and documents production-ready bundle optimization strategies that reduce reverts, maximize bundle profitability, and respect reputation constraints — without violating ERC-4337 rules.
It includes:
- A deterministic scoring engine for
UserOperationprioritization - Reputation-aware bundling (staking, ban duration, opsSeen/opsIncluded)
- Gas-price discontinuity detection across Flashbots + public mempool
- Simulated bundle dry-runs before on-chain submission
Alchemy, Stackup, Pimlico, and Biconomy compete on bundler reliability and inclusion rate. A poorly optimized bundler spams the mempool, wastes gas on reverts, and gets entities jailed. This repo provides auditable, testable heuristics that bundler teams can copy directly or adapt.
A bundler that doesn't optimize is just a relayer with extra steps.
- Architecture
- Scoring Engine
- Reputation Integration
- Mempool Simulation Rules
- Installation
- Usage
- Docs
- Contributing
- License
[UserOp Pool] → [Pre-filter] → [Scoring Engine] → [Bundle Assembly] → [Dry Run] → [Submission]
│ │ │ │
↓ ↓ ↓ ↓
(gas limit) (fee + rep) (size + time) (revert check)
The optimizer runs every block window. It:
- Filters out ops with
verificationGasLimit > block gas limit - 21000 - Scores each op using
score = (maxPriorityFeePerGas × prestake) / (gasPriceDeviation + 1) - Sorts bundles by cumulative score, then simulates top 3 candidates
- Selects the bundle with the highest
executionRatio(successful ops / total ops)
interface OptimizedUserOperation {
userOp: UserOperation;
score: number;
prestake: bigint; // from reputation store
}
function scoreOperation(
op: UserOperation,
reputation: ReputationEntry
): number {
const fee = Number(op.maxPriorityFeePerGas);
const stake = Number(reputation.stake ?? 0n);
const seen = reputation.opsSeen + 1;
const deviation =
Math.abs(Number(op.maxFeePerGas) - medianGasPrice()) / 1e9;
// Reward high fee and stake; punish deviation and excessive submissions
return (fee * stake) / (deviation + 1) / seen;
}
function selectBundle(
ops: OptimizedUserOperation[],
maxSize: number
): OptimizedUserOperation[] {
return ops
.sort((a, b) => b.score - a.score)
.reduce((bundle, op) => {
const size = estimateBundleSize([...bundle, op]);
return size <= maxSize ? [...bundle, op] : bundle;
}, [] as OptimizedUserOperation[]);
}Two scoring strategies are available under src/optimizer/scoring/:
| Strategy | Best for |
|---|---|
linear.ts |
Stable mempool, low volatility |
dynamic.ts |
High-volatility periods, MEV competition |
ERC-4337 defines reputation rules but not their implementation. This repo provides a SQLite-backed reputation store with full throttling, banning, and recovery logic.
| Field | Type | Description |
|---|---|---|
address |
string |
Entity: paymaster, factory, or aggregator |
opsSeen |
number |
Total UserOps seen from this entity |
opsIncluded |
number |
Ops successfully included on-chain |
status |
ok | throttled | banned |
Current entity status |
banUntil |
number |
Unix timestamp of ban expiry |
Core rule: If opsSeen > 100 and opsIncluded / opsSeen < 0.1 → entity is throttled for 10 minutes.
See docs/reputation-algorithms.md for full throttling, banning, and unstaking delay logic.
Before a UserOperation enters the bundle pool, it passes through simulateValidation():
UserOperation → simulateValidation()
↓
EntryPoint.simulateValidation()
↓
[Verification gas limit] → reject if >30% of block gas
[Factory/Paymaster stake] → reject if stake < 1 ETH
[Signature replay check] → reject if nonce gap > 64
↓
✓ Pass → enter bundle pool
✗ Fail → drop + update reputation
Note: simulateValidation() passing does not guarantee handleOps() success. See docs/bundle-simulation-edge-cases.md.
git clone https://github.com/enochTe8/erc4337-bundle-optimizer.git
cd erc4337-bundle-optimizer
npm install
cp .env.example .env
# Add: RPC_URL, ENTRYPOINT_ADDRESS, BUNDLER_PRIVATE_KEYRequirements: Node.js 18+, TypeScript 5.x
import { BundleOptimizer } from './src/optimizer';
import { MempoolMonitor } from './src/mempool';
const optimizer = new BundleOptimizer({
entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789',
reputationStore: './reputation.db', // or 'sqlite::memory:'
maxBundleSize: 3_000_000, // 3M gas
scoringStrategy: 'dynamic',
});
const monitor = new MempoolMonitor(optimizer);
monitor.startPolling(2000); // poll every 2 seconds
monitor.on('bundle:submitted', ({ txHash, ops, profit }) => {
console.log(`Bundle submitted: ${txHash} | ops: ${ops} | profit: ${profit} gwei`);
});Run the CLI directly:
npm run optimize -- --rpc https://mainnet.infura.io/v3/YOUR_KEY --dry-run| Document | Description |
|---|---|
docs/reputation-algorithms.md |
Throttling, banning, and unstaking delay logic |
docs/bundle-simulation-edge-cases.md |
Why validation can pass but handleOps() reverts |
docs/gas-price-discontinuity.md |
Detecting gas spikes across Flashbots + public mempool |
See CONTRIBUTING.md. High-value contributions:
- ERC-4337 v0.7
EntityTypesupport - Time-based bundle expiry for long-pending ops
- Prometheus metrics endpoint for bundler ROI tracking
- Flashbots bundle submission integration
MIT — free to use for bundler operators and infrastructure teams. Attribution appreciated.
Written and maintained by Enoch Agboola — Account Abstraction technical writer.