Building a Liquidation Bot: RPC Requirements and Infrastructure Guide
Liquidation bots keep DeFi lending protocols solvent. When a borrower’s collateral value drops below the liquidation threshold, liquidators can repay their debt and claim the collateral at a discount. The bot that gets there first wins the liquidation.
This guide covers the RPC infrastructure a production liquidation bot needs: not the strategy theory, but the actual connection patterns, call sequences, and failover logic that determine whether your bot lands liquidations or watches others take them.
How Liquidation Bots Use RPC
A liquidation bot makes three types of RPC calls continuously:
1. Position scanning: reading health factors across all open positions 2. Opportunity detection: identifying positions below liquidation threshold 3. Execution: submitting the liquidation transaction fast enough to win
Each phase has different RPC requirements.
Phase 1: Position Scanning with Multicall3
Scanning thousands of positions one by one via individual eth_call requests is too slow. Use Multicall3 to batch all health factor reads into as few requests as possible.
import { ethers } from 'ethers';
const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
const MULTICALL3_ABI = [
'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) view returns (tuple(bool success, bytes returnData)[] returnData)',
];
// Aave V3 health factor interface
const AAVE_POOL_ABI = [
'function getUserAccountData(address user) view returns (uint256 totalCollateralBase, uint256 totalDebtBase, uint256 availableBorrowsBase, uint256 currentLiquidationThreshold, uint256 ltv, uint256 healthFactor)',
];
const provider = new ethers.JsonRpcProvider(
'https://eu.endpoints.matrixed.link/rpc/ethereum?auth=YOUR_KEY'
);
const multicall = new ethers.Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
const poolInterface = new ethers.Interface(AAVE_POOL_ABI);
async function scanHealthFactors(borrowers, poolAddress) {
// Batch all health factor reads into one RPC call
const calls = borrowers.map(borrower => ({
target: poolAddress,
allowFailure: true,
callData: poolInterface.encodeFunctionData('getUserAccountData', [borrower]),
}));
const results = await multicall.aggregate3(calls);
return borrowers.map((borrower, i) => {
if (!results[i].success) return { borrower, healthFactor: null };
const decoded = poolInterface.decodeFunctionResult(
'getUserAccountData',
results[i].returnData
);
return {
borrower,
healthFactor: decoded.healthFactor,
totalCollateralBase: decoded.totalCollateralBase,
totalDebtBase: decoded.totalDebtBase,
liquidatable: decoded.healthFactor < ethers.parseUnits('1', 18),
};
});
}
// Scan 500 positions in one RPC call
const positions = await scanHealthFactors(borrowerAddresses, AAVE_V3_POOL);
const liquidatable = positions.filter(p => p.liquidatable);
Phase 2: Getting Borrower Lists
You need a list of all open positions to scan. Two approaches:
Option A: eth_getLogs (historical + ongoing)
// Aave V3 emits events when positions are opened
const borrowTopic = ethers.id('Borrow(address,address,address,uint256,uint8,uint256,uint16)');
const currentBlock = await provider.getBlockNumber();
// Get all borrowers from last 30 days
const logs = await provider.getLogs({
address: AAVE_V3_POOL,
topics: [borrowTopic],
fromBlock: currentBlock - 216000, // ~30 days at 12s/block
toBlock: 'latest',
});
// Extract unique borrower addresses
const borrowers = [...new Set(
logs.map(log => {
const decoded = poolInterface.parseLog(log);
return decoded.args.onBehalfOf;
})
)];
Option B: WebSocket subscription (new positions only)
const wsProvider = new ethers.WebSocketProvider(
'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
);
const pool = new ethers.Contract(AAVE_V3_POOL, AAVE_POOL_ABI, wsProvider);
// Add new borrowers to your list as they open positions
pool.on('Borrow', (reserve, user, onBehalfOf, amount, interestRateMode, borrowRate, referralCode) => {
if (!borrowerSet.has(onBehalfOf)) {
borrowerSet.add(onBehalfOf);
console.log(`New borrower: ${onBehalfOf}`);
}
});
In production, combine both: load historical borrowers on startup, add new ones via subscription.
Phase 3: Continuous Health Factor Monitoring
Scan all positions every block. Prices move fast. A position that was healthy 12 seconds ago may be liquidatable now.
const wsProvider = new ethers.WebSocketProvider(WSS_URL);
// Trigger scan on every new block
wsProvider.on('block', async (blockNumber) => {
console.log(`Block ${blockNumber} — scanning ${borrowers.size} positions`);
const positions = await scanHealthFactors([...borrowers], AAVE_V3_POOL);
const liquidatable = positions.filter(p => p.liquidatable);
if (liquidatable.length > 0) {
console.log(`Found ${liquidatable.length} liquidatable positions`);
await Promise.all(liquidatable.map(pos => attemptLiquidation(pos, blockNumber)));
}
});
Phase 4: Simulating Before Executing
Always simulate before submitting. A failed liquidation transaction still costs gas.
const LIQUIDATION_ABI = [
'function liquidationCall(address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken) returns ()',
];
async function simulateLiquidation(position, collateralAsset, debtAsset, debtToCover) {
const pool = new ethers.Contract(AAVE_V3_POOL, LIQUIDATION_ABI, provider);
try {
// Dry run — no gas spent
await pool.liquidationCall.staticCall(
collateralAsset,
debtAsset,
position.borrower,
debtToCover,
false, // receive underlying, not aToken
);
// Estimate gas for actual submission
const gasEstimate = await pool.liquidationCall.estimateGas(
collateralAsset,
debtAsset,
position.borrower,
debtToCover,
false,
);
return { profitable: true, gasEstimate };
} catch (error) {
// Would revert — position no longer liquidatable or already taken
return { profitable: false, reason: error.reason };
}
}
Phase 5: Execution - Standard vs Flashbots
Standard submission (public mempool): fast to submit, visible to frontrunners.
async function executeLiquidation(position, collateralAsset, debtAsset, debtToCover, gasEstimate) {
const pool = new ethers.Contract(AAVE_V3_POOL, LIQUIDATION_ABI, wallet);
const feeData = await provider.getFeeData();
const tx = await pool.liquidationCall(
collateralAsset,
debtAsset,
position.borrower,
debtToCover,
false,
{
gasLimit: gasEstimate * 120n / 100n, // 20% buffer
maxFeePerGas: feeData.maxFeePerGas * 2n, // Aggressive fee to win
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas * 2n,
}
);
const receipt = await tx.wait();
console.log(`Liquidation confirmed. Block: ${receipt.blockNumber}, Gas: ${receipt.gasUsed}`);
}
Flashbots submission (private mempool): prevents frontrunning, recommended for competitive liquidations.
import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle';
const flashbotsProvider = await FlashbotsBundleProvider.create(
provider,
ethers.Wallet.createRandom(), // reputation signer
'https://relay.flashbots.net'
);
async function executeViaFlashbots(signedLiquidationTx, targetBlock) {
const bundle = [{ signedTransaction: signedLiquidationTx }];
// Simulate bundle
const simulation = await flashbotsProvider.simulate(bundle, targetBlock);
if ('error' in simulation) {
console.error('Bundle would fail:', simulation.error);
return;
}
// Submit to next 3 blocks for higher inclusion probability
for (let i = 0; i < 3; i++) {
await flashbotsProvider.sendRawBundle(bundle, targetBlock + i);
}
}
Infrastructure Architecture
WebSocket Provider (WSS)
→ block subscriptions (trigger scans)
→ Borrow/Repay event subscriptions (maintain borrower list)
HTTP Provider (HTTPS)
→ Multicall3 health factor scans (every block)
→ staticCall simulations (before execution)
→ Transaction submission (fallback to public mempool)
Flashbots Relay
→ Bundle simulation (eth_callBundle)
→ Private bundle submission
Backup HTTP Provider
→ Automatic failover if primary is slow or down
// Production provider setup
const primary = new ethers.JsonRpcProvider(PRIMARY_RPC_URL);
const backup = new ethers.JsonRpcProvider(BACKUP_RPC_URL);
const provider = new ethers.FallbackProvider([
{ provider: primary, priority: 1, weight: 2, stallTimeout: 1500 },
{ provider: backup, priority: 2, weight: 1, stallTimeout: 2000 },
]);
RPC Requirements Checklist
Before going live, verify your provider supports:
| Requirement | Why |
|---|---|
| WebSocket subscriptions | Block and event monitoring |
eth_call at high rate | Multicall3 scans every block |
| Low HTTP latency (<100ms) | Simulation speed matters |
| High rate limits | Scanning 1,000 positions/block = high call volume |
| Reliable uptime | Downtime = missed liquidations |
eth_getLogs for large ranges | Historical borrower list on startup |
FAQ
How many RPC calls does a liquidation bot make per block?
It depends on position count. For 1,000 borrowers using Multicall3: 2-4 Multicall3 calls per block (batching 250-500 positions per call) plus WebSocket subscription traffic. Without Multicall3: up to 1,000 individual calls per block, which is unsustainable on most plans.
Is Flashbots required for liquidation bots?
Not required, but recommended for competitive protocols. On Ethereum mainnet, liquid liquidations are heavily competed. Public mempool submissions get frontrun frequently. Flashbots private submission protects your liquidation from being sandwiched.
What happens if I scan a position but someone else liquidates it first?
Your staticCall simulation will revert when you try. The position is no longer liquidatable. This is normal. The simulation catches it before you spend gas on a failing transaction.
How do I handle protocols other than Aave?
Each protocol has its own health factor calculation and liquidation interface. Compound uses getAccountLiquidity, Morpho has its own accounting, etc. The RPC patterns (Multicall3 scanning, event log indexing, staticCall simulation) apply to all of them. Only the contract interfaces differ.
What is the minimum latency needed to be competitive?
It varies by protocol and chain. On Ethereum mainnet for major protocols, sub-50ms round-trip time is competitive. On L2s (Arbitrum, Base) where blocks are faster, lower latency matters more. Co-locate your bot server in the same region as your RPC provider to minimize round-trip time.
BoltRPC provides HTTP and WebSocket access to Ethereum, Arbitrum, Base, Polygon, Optimism, Avalanche and 16 other networks, covering all the chains where active DeFi liquidation opportunities exist.
Start your free 2-week trial: trial.boltrpc.io
Related: Optimizing RPC Calls for DeFi | MEV Bots and RPC Infrastructure