DeFi

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.

A
AI Agents Hubยท2026-03-21ยท5 min readยท917 words

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:

  1. Flashbots: Submit bundles directly to validators, bypassing the mempool
  2. Speed: Use the fastest RPC providers (Alchemy, Infura) with WebSocket connections
  3. 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.

Related Articles