How to Build a Liquidation Bot for Aave and Compound
Liquidation bots monitor undercollateralized loans on Aave, Compound, and other DeFi lending protocols. When a position drops below the liquidation threshold, your bot earns a 5-10% bonus. Step-by-step guide with Solidity and Python.
Builder of AI agents, crypto trading bots, and open-source automation tools. Sharing practical guides on how to build, deploy, and profit from AI and DeFi technology.
What Is a Liquidation Bot?
DeFi lending protocols like Aave, Compound, and Spark require borrowers to maintain collateral above a minimum threshold. When collateral drops below this threshold, the position can be liquidated by anyone โ and the liquidator earns a 5-10% bonus on the liquidated amount.
This bonus is your profit. In volatile markets, hundreds of positions can become liquidatable in minutes.
Understanding Health Factors
On Aave, every borrowing position has a health factor:
- Health Factor > 1.0: Safe
- Health Factor = 1.0: At liquidation threshold
- Health Factor < 1.0: Liquidatable (can be liquidated by anyone)
Health Factor = (collateral ร liquidation_threshold) / total_debt
A position with $10,000 ETH collateral and $6,000 USDC debt at 80% threshold:
HF = ($10,000 ร 0.80) / $6,000 = 1.33 โ
Safe
If ETH drops 25%, collateral = $7,500:
HF = ($7,500 ร 0.80) / $6,000 = 1.0 โ ๏ธ At threshold
Another 1% drop โ HF < 1.0 โ Liquidatable.
Monitoring Liquidatable Positions
from web3 import Web3
import json
import time
w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'))
AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2'
AAVE_POOL_ABI = [
{
"name": "getUserAccountData",
"type": "function",
"inputs": [{"name": "user", "type": "address"}],
"outputs": [
{"name": "totalCollateralBase", "type": "uint256"},
{"name": "totalDebtBase", "type": "uint256"},
{"name": "availableBorrowsBase", "type": "uint256"},
{"name": "currentLiquidationThreshold", "type": "uint256"},
{"name": "ltv", "type": "uint256"},
{"name": "healthFactor", "type": "uint256"},
]
},
{
"name": "liquidationCall",
"type": "function",
"inputs": [
{"name": "collateralAsset", "type": "address"},
{"name": "debtAsset", "type": "address"},
{"name": "user", "type": "address"},
{"name": "debtToCover", "type": "uint256"},
{"name": "receiveAToken", "type": "bool"},
]
}
]
pool = w3.eth.contract(address=AAVE_V3_POOL, abi=AAVE_POOL_ABI)
def get_health_factor(user_address: str) -> float:
"""Get user's current health factor on Aave V3"""
data = pool.functions.getUserAccountData(user_address).call()
health_factor = data[5] / 1e18 # Health factor is in 18 decimal format
return health_factor
def is_liquidatable(user_address: str) -> bool:
hf = get_health_factor(user_address)
return hf < 1.0 and hf > 0 # hf=0 means no debt
def get_user_positions(user_address: str) -> dict:
"""Get detailed position info"""
data = pool.functions.getUserAccountData(user_address).call()
return {
'address': user_address,
'collateral_usd': data[0] / 1e8, # Base currency is 8 decimals
'debt_usd': data[1] / 1e8,
'health_factor': data[5] / 1e18,
'liquidatable': data[5] / 1e18 < 1.0,
}
Finding Liquidatable Positions (The Efficient Way)
Rather than checking every address (impossible), monitor Aave events for borrow activity:
from web3 import Web3
from web3.middleware import ExtraDataToPOAMiddleware
import asyncio
# Track all addresses that have borrowed
MONITORED_ADDRESSES = set()
def track_borrowers_from_events():
"""Build a list of all Aave borrowers by scanning Borrow events"""
BORROW_EVENT_SIGNATURE = w3.keccak(
text='Borrow(address,address,address,uint256,uint8,uint256,uint16)'
).hex()
# Get last 100,000 blocks of borrow events
current_block = w3.eth.block_number
start_block = current_block - 100000
events = w3.eth.get_logs({
'address': AAVE_V3_POOL,
'topics': [BORROW_EVENT_SIGNATURE],
'fromBlock': start_block,
'toBlock': current_block,
})
for event in events:
# Borrower address is the 3rd topic (indexed)
borrower = w3.to_checksum_address('0x' + event['topics'][2].hex()[-40:])
MONITORED_ADDRESSES.add(borrower)
print(f"Tracking {len(MONITORED_ADDRESSES)} Aave borrowers")
async def scan_for_liquidations():
"""Continuously scan known borrowers for liquidatable positions"""
while True:
liquidatable = []
for address in list(MONITORED_ADDRESSES):
try:
hf = get_health_factor(address)
if hf < 1.05: # Alert when close to liquidation
position = get_user_positions(address)
if hf < 1.0:
print(f"๐ฏ LIQUIDATABLE: {address} | HF: {hf:.4f} | Debt: ${position['debt_usd']:,.2f}")
liquidatable.append(position)
elif hf < 1.05:
print(f"โ ๏ธ WATCH: {address} | HF: {hf:.4f} | Debt: ${position['debt_usd']:,.2f}")
except Exception:
pass
await asyncio.sleep(0.1) # Rate limit RPC calls
if liquidatable:
await process_liquidations(liquidatable)
await asyncio.sleep(12) # One Ethereum block = ~12 seconds
Executing a Liquidation with Flash Loans
The trick: you need debt repayment capital upfront. Solution: use Aave's own flash loans.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import "@aave/core-v3/contracts/interfaces/IPool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract AaveLiquidator is FlashLoanSimpleReceiverBase {
address public owner;
constructor(address _addressProvider) FlashLoanSimpleReceiverBase(_addressProvider) {
owner = msg.sender;
}
// Called by Aave after sending flash loan
function executeOperation(
address asset,
uint256 amount,
uint256 premium, // Flash loan fee
address initiator,
bytes calldata params
) external override returns (bool) {
// Decode liquidation params
(
address collateralAsset,
address borrower
) = abi.decode(params, (address, address));
// Approve Aave to use flash loaned tokens for repayment
IERC20(asset).approve(address(POOL), amount);
// Liquidate: repay debt, receive collateral + bonus
POOL.liquidationCall(
collateralAsset,
asset, // debt asset
borrower,
amount, // amount to repay
false // receive underlying token (not aToken)
);
// Swap received collateral back to debt asset (via Uniswap/1inch)
uint256 collateralReceived = IERC20(collateralAsset).balanceOf(address(this));
_swapToRepayAsset(collateralAsset, asset, collateralReceived);
// Repay flash loan + fee
uint256 amountOwed = amount + premium;
IERC20(asset).approve(address(POOL), amountOwed);
// Profit = balance after repayment
uint256 profit = IERC20(asset).balanceOf(address(this)) - amountOwed;
require(profit > 0, "No profit from liquidation");
return true;
}
function liquidate(
address collateralAsset,
address debtAsset,
address borrower,
uint256 debtAmount
) external {
require(msg.sender == owner, "Not owner");
bytes memory params = abi.encode(collateralAsset, borrower);
// Request flash loan for debt amount
POOL.flashLoanSimple(
address(this),
debtAsset,
debtAmount,
params,
0 // referral code
);
}
function _swapToRepayAsset(
address tokenIn,
address tokenOut,
uint256 amountIn
) internal {
// Implement Uniswap V3 or 1inch swap here
// ... swap logic ...
}
function withdraw(address token) external {
require(msg.sender == owner, "Not owner");
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(owner, balance);
}
}
Competition and MEV
Liquidation bots are heavily competitive. The Ethereum mempool is monitored by MEV bots that will front-run your transactions. To compete:
- Flashbots: Submit bundles directly to validators, bypassing the mempool
- Speed: Use the fastest RPC providers (Alchemy, Infura) with WebSocket connections
- Gas: Bid higher gas to get included first
For beginners, L2 chains (Arbitrum, Base) are less competitive and a better starting point than Ethereum mainnet.
Liquidation bots earned an estimated $50M+ in 2025. The opportunity is real โ but so is the competition.
Tagged in
Related Articles
Uniswap V4 Hooks: The New DeFi Bot Primitive Every Developer Must Know
5 min read
DeFiEthereum Layer 2 Trading Bot Comparison: Arbitrum vs Base vs Optimism
5 min read
DeFiSolana vs Ethereum for DeFi Bots: Which Blockchain Wins in 2026?
4 min read
DeFiWhat Are Real World Assets (RWA) in DeFi and How Bots Can Trade Them
5 min read