Aave liquidations returned $890M in bonuses in 2025. Most went to bots. Here's how to build one that competes.
Your first attempt will fail. You'll spend hours watching positions slip below a health factor of 1.0, only to see tx reverted: position already liquidated in your console. The mempool is a warzone, and your vanilla RPC calls are moving at the speed of a dial-up modem while the competition operates at fiber-optic latency. This guide is about building a predator, not a scavenger. We'll cover the full stack: from detecting underwater positions in under 50ms, to executing profitable liquidations via flash loans, to protecting your profits from MEV bots that would gladly sandwich your hard-earned bonus into their own pocket.
Aave V3 Liquidation Mechanics: The Math Behind the Bonus
Forget the marketing fluff. A liquidation is a forced sale of a borrower's collateral to repay their debt, triggered when their Health Factor (HF) drops below 1.0. The HF is the ratio of the borrower's collateral (in USD) to their debt (in USD), adjusted by the Loan-to-Value (LTV) ratio. Think of it as the borrower's safety margin.
Health Factor = (Total Collateral in USD * Liquidation Threshold) / Total Debt in USD
When HF < 1, the position is liquidatable. The liquidator repays up to 50% (or up to 100% if the debt is in a stablecoin) of the borrower's outstanding debt in a single transaction. In return, they receive the corresponding collateral, plus a liquidation bonus. This bonus is your profit. On Aave V3, it's typically between 5% and 10%, depending on the asset. You repay $100k of USDC debt, you might get $105k worth of ETH back. That $5k is your prize.
The catch? You need the capital to repay that debt. Enter the flash loan.
Position Monitoring: The Graph vs. RPC Polling — The 1,600x Speed Difference
You can't liquidate what you can't see. The naive approach is to poll an Ethereum RPC node for every user's health factor. This is like trying to drink from a firehose with a straw—you'll miss everything and get waterboarded by rate limits.
The professional method is to use a pre-indexed data layer. Here’s the brutal efficiency comparison:
| Method | Avg. Query Time | Data Freshness | Setup Complexity | Best For |
|---|---|---|---|---|
| The Graph Subgraph | 5ms | ~1 block delay | High (need to write/subgraph) | Real-time bot detection |
| Dune Analytics SQL | 8,000ms | ~5 min delay | Medium | Post-mortem analysis, dashboards |
| Direct RPC Polling | 200-500ms | Immediate | Low | Single-position checks |
That's a 1,600x speed advantage for The Graph. For a MEV bot latency requirement of <50ms from detection to bundle submission, RPC polling is a non-starter. You need subgraphs.
Aave provides official subgraphs. Here's how you query for potentially liquidatable positions on Ethereum mainnet using the Aave V3 subgraph and ethers.js:
import { ethers } from 'ethers';
import { request } from 'graphql-request';
const AAVE_V3_SUBGRAPH = 'https://api.thegraph.com/subgraphs/name/aave/protocol-v3';
const LIQUIDATION_QUERY = `
query GetUnhealthyPositions($healthFactorThreshold: String!) {
userReserves(where: {healthFactor_lt: $healthFactorThreshold, healthFactor_gt: "0"}) {
user {
id
}
reserve {
symbol
underlyingAsset
price {
priceInEth
}
decimals
}
scaledATokenBalance
currentVariableDebt
healthFactor
}
}
`;
async function findLiquidatablePositions() {
try {
const data = await request(AAVE_V3_SUBGRAPH, LIQUIDATION_QUERY, {
healthFactorThreshold: '1.05' // Look for positions below 1.05 for a buffer
});
return data.userReserves.map(ur => ({
user: ur.user.id,
debtAsset: ur.reserve.symbol,
debtAmount: ur.currentVariableDebt,
collateralAsset: '...', // You'd need to join on collateral reserves
healthFactor: ur.healthFactor
}));
} catch (error) {
console.error('Subgraph query failed:', error);
}
}
This gets you the list. But is it profitable?
Profitability Check: Net Profit After Gas, Slippage, and Bonus
Seeing a position with HF = 0.99 is not a green light. It's a hypothesis. You must prove it's profitable.
Your net profit is:
Net Profit = (Collateral Received Value) - (Debt Repaid Value) - (Transaction Gas Cost) - (Flash Loan Fee) - (Swap Slippage)
Miss any component, and you execute a trade that loses money. The most common real error message you'll see in your simulations is:
Error: Flash loan callback not profitable after gas
Fix: Calculate gas cost at current base fee and priority fee before building your transaction. Include a hardcoded profitability check inside your flash loan execution callback.
// Inside your Flash Loan callback function (executeOperation)
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external override returns (bool) {
// ... your liquidation logic happens here ...
uint256 collateralReceived = sellCollateralForDebtAsset();
uint256 debtToRepay = amounts[0] + premiums[0]; // Borrowed amount + 0.05% fee
uint256 profit = collateralReceived - debtToRepay;
// CRITICAL: On-chain profitability guard
uint256 estimatedGasCost = tx.gasprice * 500000; // Estimate gas for the rest of tx
require(profit > estimatedGasCost, "Flash loan callback not profitable after gas");
// Repay the flash loan
IERC20(assets[0]).approve(address(POOL), debtToRepay);
return true;
}
Flash Loan Integration: The Engine of Capital-Efficient Liquidations
You don't need your own capital. Aave V3 flash loans let you borrow up to the full reserve size, provided you repay it + a 0.05% fee in the same transaction. The flow is atomic: borrow, liquidate, swap collateral for debt asset, repay, keep the excess.
Here's a Foundry test snippet that outlines the structure of a liquidation bot contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {IPool} from "@aave-v3-core/interfaces/IPool.sol";
import {IFlashLoanReceiver} from "@aave-v3-core/interfaces/IFlashLoanReceiver.sol";
contract LiquidationBot is IFlashLoanReceiver {
IPool public immutable AAVE_POOL;
address public immutable owner;
constructor(address _pool) {
AAVE_POOL = IPool(_pool);
owner = msg.sender;
}
function initiateLiquidation(
address debtAsset, // e.g., USDC
uint256 debtAmount,
address collateralAsset, // e.g., WETH
address targetUser
) external payable {
// 1. Request Flash Loan
address[] memory assets = new address[](1);
assets[0] = debtAsset;
uint256[] memory amounts = new uint256[](1);
amounts[0] = debtAmount;
AAVE_POOL.flashLoan(
address(this),
assets,
amounts,
new uint256[](1), // Interest rate mode - 0 for variable debt
address(this), // Initiator
abi.encode(collateralAsset, targetUser), // Params for callback
0 // Referral code
);
// 2. Profit (if any) is now in this contract
// 3. Send profit to owner
}
function executeOperation(
// ... as shown in the previous code block ...
) external override returns (bool) {
// Full liquidation logic
}
}
MEV Protection: Submitting Bundles, Not Transactions
If you broadcast a profitable liquidation transaction to the public mempool, you are donating your work to a generalized frontrunner. They will see it, copy it, increase the gas fee, and have a validator include their version before yours. You pay the gas for their rehearsal.
Real error message: Sandwich attack on your swap — your output is 5% less than expected.
Fix: Use a private transaction relay. Flashbots is the standard. Instead of sending a transaction, you send a bundle (a sequence of transactions) directly to block builders via a relay, bypassing the public mempool. This hides your intent until it's included in a block.
import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle';
import { ethers } from 'ethers';
const provider = new ethers.providers.JsonRpcProvider(process.env.ETH_RPC_URL);
const authSigner = new ethers.Wallet(process.env.FLASHBOTS_PRIVATE_KEY, provider); // This is a special signer for Flashbots auth
const flashbotsProvider = await FlashbotsBundleProvider.create(provider, authSigner);
const signedLiquidationTx = await botContract.populateTransaction.initiateLiquidation(...);
const bundle = [
{ signer: wallet, transaction: signedLiquidationTx }
];
// Target a block 5 blocks ahead
const targetBlockNumber = (await provider.getBlockNumber()) + 5;
const bundleSubmission = await flashbotsProvider.sendBundle(bundle, targetBlockNumber);
Multi-Chain Deployment: Arbitrum and Base Where Competition Is Lower
Ethereum mainnet is the major leagues. The gas is high, but the prizes are huge (remember the $890M liquidated in a single day). The competition is fierce, often dominated by sophisticated players.
For a new bot, consider starting on L2s like Arbitrum or Base. The TVL is significant, but the bot ecosystem is less saturated. The principles are identical—Aave V3 is deployed on these chains—but the gas costs are lower, allowing you to be more aggressive with profitability thresholds. Monitor DeFi Total Value Locked per chain on DefiLlama to identify growing markets.
Risk Management: Don't Blow Up Your Own Wallet
This is advanced DeFi surgery. Your risks are not just missing opportunities.
- Smart Contract Risk: Your bot contract has a
receive()function. What if someone sends it tokens? What if the Aave pool address changes on an upgrade? Use OpenZeppelin'sOwnableandReentrancyGuard. Test extensively on forked networks using Tenderly or Foundry'sforge test --fork-url. - Oracle Risk: The liquidation trigger depends on oracle prices. If you rely on a DEX's spot price for your profitability calculation and it's manipulated via a flash loan, you could overpay for collateral. Fix: Use longer TWAP window (30 min not 5 min), cross-reference with Chainlink oracle.
- Execution Risk: Slippage on large swaps can kill profit. Always set a maximum slippage tolerance (e.g., 0.5%) and use
amountOutMinparameters in your swaps. - Position Size Limits: Never let your bot attempt to liquidate a position larger than a predefined percentage of your available gas budget or flash loan capacity. A single failed transaction on mainnet can cost 0.5 ETH.
Next Steps: From Prototype to Production
You now have the blueprint. The journey from here to a live, profitable bot involves relentless iteration.
- Fork & Simulate: Use
anvil --fork-url $RPC_URL(from Foundry) to create a local fork of mainnet. Impersonate accounts, lower their health factors manually, and run your bot against them. This is your sandbox. - Build a Backtest Engine: Use historical block data from Dune or The Graph to simulate how your bot would have performed during past liquidation events, like the ETH price drop of 18% in Jan 2026.
- Implement a Scheduler: Your monitoring function needs to run constantly. Use a lightweight Node.js script with
setIntervalor a more robust job queue like Bull. - Monitor Everything: Log every detected position, every bundle submission, every profit and loss. A missed opportunity is data. The error
Liquidation bot misses opportunity: position already liquidatedmeans your detection-to-submission pipeline is too slow. Fix: Subscribe to Aave LiquidationCall events via a WebSocket connection to an RPC, not just polling. Checkmsg.sender != liquidatorin your simulation before executing. - Start on a Testnet/L2: Deploy to Sepolia or Arbitrum Goerli. Use testnet ETH to validate the entire flow end-to-end without financial risk.
The edge in DeFi liquidation isn't just in having a bot; it's in the milliseconds you shave off detection, the basis points you save in execution, and the robustness of your code when the network is congested and gas prices spike. Your RTX 4090 is great for running local forks, but the real battle is won in the efficiency of your algorithms and the depth of your understanding. Now go build. The next $890M is waiting.