MEV Bots and RPC: Infrastructure Requirements for Searchers
Most MEV guides focus on strategy: how sandwich attacks work, how to find arbitrage opportunities, how to write Flashbots bundles. This guide focuses on the infrastructure layer that all of those strategies depend on: RPC.
The quality of your RPC setup determines whether you see opportunities, how fast you can act on them, and whether your transactions land. Getting the strategy right and the infrastructure wrong means losing to searchers with faster connections.
Why RPC Is the MEV Bottleneck
MEV extraction is a competition. Every searcher monitoring the same mempool is racing to:
- See an opportunity first
- Simulate a response transaction fastest
- Get a bundle or transaction confirmed in the next block
At each step, RPC performance is either your advantage or your bottleneck.
Step 1: Seeing opportunities:
Opportunities arrive via new blocks (eth_subscribe → newHeads) and pending transactions (eth_subscribe → newPendingTransactions). A WebSocket subscription with lower latency sees these earlier.
Step 2: Simulating responses:
Once you identify an opportunity, you simulate your response transaction using eth_call (to check profitability) and eth_estimateGas (to ensure it would succeed). These are synchronous RPC calls. Every millisecond of RPC latency adds to your simulation time.
Step 3: Landing the transaction: Private mempool submission (via Flashbots or equivalent) goes through a separate RPC endpoint. The speed and reliability of this connection determines whether your bundle reaches the block builder in time.
Required RPC Connections
A production MEV setup typically requires three separate RPC connections:
1. Public RPC (HTTP)
→ eth_call simulations
→ eth_estimateGas
→ eth_getBalance, eth_getCode
→ General state reads
2. WebSocket (Public mempool)
→ eth_subscribe newHeads (new blocks)
→ eth_subscribe newPendingTransactions (mempool)
3. Flashbots/Private RPC
→ eth_sendBundle (private bundle submission)
→ eth_callBundle (bundle simulation)
→ mev_sendBundle
WebSocket Setup for Mempool Monitoring
import { ethers } from 'ethers';
const wsProvider = new ethers.WebSocketProvider(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
);
// Monitor new blocks: recalculate positions on every block
wsProvider.on('block', async (blockNumber) => {
console.log(`New block: ${blockNumber}`);
await scanForOpportunities(blockNumber);
});
// Monitor pending transactions: the mempool view
// Note: not all providers expose full pending tx data
wsProvider.on('pending', async (txHash) => {
// Get full transaction data
const tx = await wsProvider.getTransaction(txHash);
if (!tx) return;
// Check if this transaction creates an MEV opportunity
const opportunity = await analyzeTransaction(tx);
if (opportunity) {
await respondToOpportunity(opportunity, tx);
}
});
Important: Not all RPC providers expose newPendingTransactions with full transaction data. Some return only hashes, requiring a separate eth_getTransactionByHash call per transaction. This adds latency. Verify your provider’s mempool support before committing to their infrastructure.
Fast Transaction Simulation with eth_call
Before submitting a bundle, simulate profitability using eth_call:
async function simulateArbitrage(path, amountIn) {
// Encode the arbitrage call
const callData = arbitrageContract.interface.encodeFunctionData(
'executeArbitrage',
[path, amountIn]
);
try {
const result = await provider.call({
to: ARBITRAGE_CONTRACT_ADDRESS,
data: callData,
});
// Decode profit from result
const decoded = arbitrageContract.interface.decodeFunctionResult(
'executeArbitrage',
result
);
return {
profitable: decoded.profit > 0n,
profit: decoded.profit,
gasEstimate: await provider.estimateGas({
to: ARBITRAGE_CONTRACT_ADDRESS,
data: callData,
}),
};
} catch (error) {
// Transaction would revert — not profitable
return { profitable: false, profit: 0n };
}
}
Latency matters here: If simulation takes 50ms longer than your competitor’s, they submit before you. Run your RPC provider from a server in the same region as the nodes to minimize round-trip time.
Flashbots Integration
Flashbots lets you submit transaction bundles privately. They bypass the public mempool and go directly to block builders, preventing frontrunning of your own transactions.
import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle';
// Your standard RPC provider
const provider = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
);
// Flashbots relay
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
ethers.Wallet.createRandom(), // Flashbots reputation key (not your funds wallet)
'https://relay.flashbots.net',
'mainnet'
);
async function submitBundle(signedTransactions, targetBlock) {
const bundle = signedTransactions.map(tx => ({
signedTransaction: tx
}));
// Simulate bundle first
const simulation = await flashbotsProvider.simulate(bundle, targetBlock);
if ('error' in simulation) {
console.error('Bundle simulation failed:', simulation.error);
return null;
}
console.log(`Bundle profit: ${ethers.formatEther(simulation.totalMinerReward)} ETH`);
// Submit to multiple blocks for higher inclusion probability
const submission = await flashbotsProvider.sendRawBundle(bundle, targetBlock);
return submission;
}
eth_callBundle: Simulate Before Submitting
Simulate your entire bundle atomically before submitting:
async function simulateBundle(transactions, blockNumber) {
const simulation = await flashbotsProvider.simulate(
transactions.map(tx => ({ signedTransaction: tx })),
blockNumber,
blockNumber, // state block: use current
);
if ('error' in simulation) {
throw new Error(`Bundle reverts: ${simulation.error.message}`);
}
const totalGasUsed = simulation.results.reduce(
(sum, tx) => sum + tx.gasUsed, 0n
);
const totalProfit = simulation.coinbaseDiff;
console.log(`Gas used: ${totalGasUsed}`);
console.log(`Profit: ${ethers.formatEther(totalProfit)} ETH`);
// Only submit if profitable after gas
const gasCost = totalGasUsed * simulation.results[0].gasPrice;
return totalProfit > gasCost;
}
Multi-Block Submission Strategy
A single block submission has low inclusion probability. Submit to multiple consecutive blocks:
async function submitToMultipleBlocks(bundle, currentBlock, blocksAhead = 3) {
const submissions = [];
for (let i = 1; i <= blocksAhead; i++) {
const targetBlock = currentBlock + i;
const submission = await flashbotsProvider.sendRawBundle(bundle, targetBlock);
submissions.push({ targetBlock, submission });
}
// Wait for any inclusion
const results = await Promise.allSettled(
submissions.map(({ submission }) => submission.wait())
);
const included = results.find(r => r.status === 'fulfilled' && r.value === 1);
return included ? 'included' : 'not included';
}
RPC Provider Selection for MEV
Not all RPC providers are equal for MEV use cases. Key criteria:
| Requirement | Why it matters |
|---|---|
| WebSocket with full mempool | See pending transactions with data, not just hashes |
| Low HTTP latency | Faster eth_call simulations |
| High rate limits | MEV bots make thousands of calls per block |
| Archive access | Some strategies require historical state |
| No method restrictions | debug_traceTransaction, eth_callBundle may be needed |
| Geographic proximity | Server in same region as your RPC = lower RTT |
| Reliable uptime | Downtime during active markets = missed opportunities |
Geographic matters more for MEV than any other use case. A 10ms improvement in round-trip time can be the difference between landing and losing a bundle.
Monitoring Your MEV Infrastructure
class MevInfraMonitor {
constructor(providers) {
this.providers = providers;
this.latencies = {};
}
async measureLatency(name, provider) {
const start = Date.now();
await provider.getBlockNumber();
this.latencies[name] = Date.now() - start;
}
async checkAll() {
await Promise.all(
Object.entries(this.providers).map(([name, provider]) =>
this.measureLatency(name, provider)
)
);
console.table(this.latencies);
// Alert if primary is slow
if (this.latencies.primary > 100) {
console.warn(`Primary RPC latency elevated: ${this.latencies.primary}ms`);
}
}
}
const monitor = new MevInfraMonitor({
primary: primaryProvider,
backup: backupProvider,
});
// Check every 30 seconds
setInterval(() => monitor.checkAll(), 30000);
FAQ
Do I need a dedicated node for MEV?
Dedicated nodes give the lowest possible latency and are used by top-tier searchers. For most strategies, a high-performance managed provider with low latency is sufficient. The economics depend on how much profit you are extracting. Dedicated node costs ($500-2,000/month) only make sense if your MEV profit justifies them.
Does BoltRPC support mempool access?
BoltRPC provides standard eth_subscribe WebSocket access including newPendingTransactions. Check your plan limits for pending transaction subscription volume, as mempool-heavy strategies generate high call rates.
What is the difference between Flashbots and a standard RPC?
A standard RPC submits transactions to the public mempool. Anyone can see them before inclusion, enabling frontrunning. Flashbots routes bundles directly to block builders via the MEV-Boost relay network, bypassing the public mempool. Your transactions are private until included in a block.
Can my MEV bot run on multiple chains simultaneously?
Yes. Run one connection set per chain. Each chain needs its own WebSocket for mempool/block monitoring and its own HTTP provider for simulations. Most chains have smaller searcher communities than Ethereum mainnet, making cross-chain opportunities less competitive.
What happens if my RPC provider goes down mid-block?
You miss everything that happens in the blocks where you are disconnected. For active strategies, implement automatic failover to a backup provider as described in our RPC Failover Guide. Recovery time directly costs profit.
Start your free 2-week trial: trial.boltrpc.io
Related: WebSocket vs HTTP for Blockchain RPC | Optimizing RPC Calls for DeFi