Lesson 9 — MEV in practice — mempool, ExEx, simulation
Question
MEV in production: watch mempool + simulate bundles + submit to builder. 3-component pipeline; ~1000 lines of Rust per searcher.
Principle (minimum model)
- Mempool watcher. Subscribe to pending txs; parse for opportunities. Use Alloy.
- Revm simulation. Fork mainnet; run candidate bundle through revm; compute profit. Use forked-anvil or CacheDB.
- Bundle submitter. Sign + submit to Flashbots / mev-share / custom builder. Use
eth_sendBundle. - Strategies. Liquidation (oracle-driven) + arbitrage (DEX-driven) + sandwich (mempool-driven). Each ~500-1000 lines.
- Profit optimisation. Gas + builder tip + slippage. The math is the differentiator.
- ExEx integration. Tempo's tidx + MEV searcher = combined on-chain + off-chain analysis. Production trend.
- Risk. Front-runner could front-run you. Use private submission (Flashbots) to mitigate.
- Production realities. ~$100M / year MEV captured across all searchers. Heavy competition; razor margins.
Worked example + steps
MEV in practice — mempool, ExEx, simulation
A pending tx hits the mempool. 80 milliseconds later, your bundle either landed or it didn't — beaten by a competing searcher who decoded the same tx 5ms faster, simulated the outcome 10ms faster, and submitted to the same builder 2ms ahead. MEV (Maximal Extractable Value — the profit a tx-orderer can pull out of pending transactions) is systems engineering meeting game theory at single-digit-millisecond timescales. This lesson is the shape of a serious 2026-era pipeline.
1. The pipeline
flowchart LR
M[Mempool<br/>ExEx + devp2p] --> D[Decoder<br/>Alloy sol!]
D --> S[Simulator<br/>Revm + DB]
S --> St[Strategy<br/>Rust logic]
St --> B[Bundle Builder<br/>Alloy encode]
B --> Sub[Submit<br/>Flashbots / direct]
Each box is a Rust module. Latency budget for the whole loop in production: < 100 ms before the next block.
2. Mempool ingest
Two paths:
- Mempool subscription via Alloy WebSocket (
pending_transactionsfilter) — easy, slow - devp2p directly — you join the network, receive raw transaction announcements, parse RLP yourself — fastest, hardest
For a serious searcher, devp2p is non-negotiable. Reth exposes it via its networking crates.
3. Decoding — the real ExEx pattern
paradigmxyz/reth-exex-examples/op-bridge is a real production-shaped indexer that decodes contract events from every block. Here's the core decoding pattern, lifted verbatim:
use alloy_sol_types::{sol, SolEventInterface};
sol!(L1StandardBridge, "l1_standard_bridge_abi.json");
use crate::L1StandardBridge::{
ETHBridgeFinalized, ETHBridgeInitiated, L1StandardBridgeEvents,
};
fn decode_chain_into_events(
chain: &Chain,
) -> impl Iterator<Item = (...)> {
chain
.blocks_and_receipts()
.flat_map(|(block, receipts)| {
block.body().transactions_iter()
.zip(receipts.iter())
.map(move |(tx, receipt)| (block, tx, receipt))
})
.flat_map(|(block, tx, receipt)| {
receipt.logs.iter()
.filter(|log| OP_BRIDGES.contains(&log.address))
.map(move |log| (block, tx, log))
})
.filter_map(|(block, tx, log)| {
L1StandardBridgeEvents::decode_raw_log(log.topics(), &log.data.data)
.ok()
.map(|event| (block, tx, log, event))
})
}
This is production-shape MEV decoding. Two flat_maps and a filter_map, stacked:
chain.blocks_and_receipts()— every block in the committed chain, paired with its receipts- For each (block, receipt) — zip transactions with their receipts and flatten
- For each (block, tx, receipt) — filter logs to known bridge addresses, then decode
The final filter_map is where sol! earns its place. L1StandardBridgeEvents::decode_raw_log is auto-generated; it tries each variant of the event enum and returns Ok(Event) on the matching topic0. No hand-rolled ABI parsing. Type-safe on the way out.
Then you pattern-match on the typed event:
match event {
L1StandardBridgeEvents::ETHBridgeInitiated(ETHBridgeInitiated {
amount, from, to, ..
}) => {
// Insert deposit into your DB
}
L1StandardBridgeEvents::ETHBridgeFinalized(ETHBridgeFinalized {
amount, from, to, ..
}) => {
// Insert withdrawal into your DB
}
_ => continue,
}
For an MEV searcher, replace "bridge addresses" with "DEX router addresses" and the deposit/withdrawal handlers with "swap detection + sandwich opportunity scoring." Same shape, different filter set.
4. Simulation
Pre-trade, you simulate against a forked state with Revm. Real shape:
use revm::Evm;
use revm::primitives::{TxKind, U256};
let mut evm = Evm::builder()
.with_db(forked_db) // mainnet state at block N
.with_external_context(())
.build();
evm.cfg_mut().chain_id = 1;
evm.tx_mut().caller = bot_address;
evm.tx_mut().transact_to = TxKind::Call(target);
evm.tx_mut().data = tx_data;
let result = evm.transact()?;
let profit = compute_profit(&result.state);
A bundled simulation (your tx + the victim tx + your tx) tells you the realized profit before you pay gas. Hot path; profile aggressively. The forked_db is typically built on AlloyDB (which we saw in the Database trait lesson) plus an LRU cache layer so identical reads don't re-hit the network.
5. ExEx as a private mempool
ExEx receives every block at zero latency. That makes it the perfect place for:
- A custom indexer of DEX trades
- A "warm cache" of pool reserves so simulation doesn't have to re-fetch
- A reorg-aware state diff feed
Your searcher consumes the ExEx feed and spends the saved time on simulation.
6. Bundle submission
Two main routes:
| Route | Latency | Privacy |
|---|---|---|
| Flashbots / MEV-share | medium | strong (no public mempool) |
| Direct to a builder | low | depends on builder |
Bundles are JSON-RPC; the wire format is small. Race between competing searchers is decided in single-digit milliseconds.
7. Things that will burn you
- Reorgs. Your bundle can land, then disappear. Always reconcile with reality after
ChainReorgednotifications in ExEx. - Stale state in the simulator. Use the exact parent block of the slot you're targeting, not "latest."
- Gas griefing. Adversaries publish high-gas transactions just to push yours out. Pay attention to the priority fee curve in real time.
- Toxic flow. Some "opportunities" are sandwich bait. Run a classifier; not all profit is real.
Final check: your bundle landed in block 1000. The chain reorgs and block 1000 is replaced. Where is your money — your ETH, the victim's ETH, the gas you paid? Trace the P&L through the reorg. If you can't, you don't yet understand why the ChainReverted handler exists in ExEx — re-read the Intermediate ExEx lesson.
Summary (3 lines)
- MEV pipeline = mempool watcher + revm simulation + bundle submitter. ~1000 lines of Rust per strategy.
- Strategies: liquidation / arbitrage / sandwich. Profit math is the differentiator.
- Risk: front-running mitigated via Flashbots private. ExEx + searcher integration = production trend. ~$100M/year captured.