Paboxo

Run a liquidator

Find unhealthy positions, repay their debt, and take the collateral at a discount. A starter script, and what it does under the hood.

Anyone can liquidate on Paboxo. There is no allowlist and no keeper role: when a position's debt exceeds its collateral value times the market's liquidation threshold, any address can repay part of that debt — up to the close factor — and receive the borrower's collateral plus the market's bonus.

Paboxo runs its own liquidator, but a lending market should not depend on one bot. This page is for developers who want to run another.

What you earn

You repay debt in USDG and receive collateral worth repaid × (1 + bonus) at oracle prices:

MarketBonusYou repayYou receive
NVDA / USDG8%1,000 USDG$1,080 of NVDA
SPY / USDG6%1,000 USDG$1,060 of SPY
WETH / USDG5%1,000 USDG$1,050 of WETH
SPCX / USDG10%1,000 USDG$1,100 of SPCX
TSLA / USDG10%1,000 USDG$1,100 of TSLA
MSFT / USDG8%1,000 USDG$1,080 of MSFT
AAPL / USDG8%1,000 USDG$1,080 of AAPL
QQQ / USDG7%1,000 USDG$1,070 of QQQ
GOOGL / USDG8%1,000 USDG$1,080 of GOOGL
AMD / USDG10%1,000 USDG$1,100 of AMD

A share of the bonus can be routed to the protocol (liquidationProtocolShare). It is 0% today, so the whole bonus is yours. Your costs are gas and the price you get when you sell the collateral: you receive the token itself (NVDA, SPY, SPCX, TSLA, MSFT, AAPL, QQQ, GOOGL, AMD or WETH), not USDG. The full arithmetic, including partial liquidations and bad debt, is on liquidation math.

Liquidating NVDA, SPY, SPCX, TSLA, MSFT, AAPL, QQQ, GOOGL or AMD means receiving Stock Tokens, which are not offered to US, Canadian, UK or Swiss persons. The contracts do not check where you are; the restriction still applies to you. Read restrictions before you run a liquidator on those markets.

Close factor

You cannot always repay the whole debt. One liquidation may repay at most:

Health factorMost you may repay
between 0.95 and 1the fraction (1.05 − hf) / (1.05 − LT × (1 + bonus)) of the debt, which brings the health factor back to 1.05
0.95 or belowthe whole debt

A request above the limit is capped, not reverted, and that includes LendingPool.liquidation(borrower). A partial liquidation must also leave at least closeFactorMinBase() / 2 — $5 today — of debt and of collateral, or it reverts MustNotLeaveDust(leftoverDebt, leftoverCollateral, minLeftover). When restoring 1.05 would leave less than that, the whole position may go.

For a bot this comes down to three rules:

  1. Read IsHealthy.maxLiquidatableShares(user, router) and repay min(that, what you can afford). It already applies every rule above, and it is 0 for a healthy position.
  2. Repaying exactly that maximum never reverts as dust. Only a smaller partial can, and only when the maximum is the whole debt; if you cannot afford it, repay at most what leaves $5 behind.
  3. Do not split. Several liquidations never repay more than one would: once the first lifts the health factor above 1, the next one reverts AssetNotLiquidatable.

So expect a slightly unhealthy position to pay a fraction of what a full liquidation would. On NVDA/USDG at a health factor of 0.975, one liquidation repays 21.55% of the debt.

The starter script

One TypeScript file with one dependency (viem). It finds every position with debt, prints its health factor and the estimated profit of liquidating it, and liquidates when you tell it to. Read it before you run it: it is short so that you can, and it is meant as the starting point of your own bot.

mkdir paboxo-liquidator && cd paboxo-liquidator
curl -O https://docs.paboxo.xyz/tools/liquidate.ts
npm init -y && npm i viem
npx tsx liquidate.ts scan

scan reads only. Run on 19 October 2026, before anyone had borrowed, it printed:

mainnet (chain 4663): 0 positions with debt

With debt on the books it prints one line per position: market, borrower, health factor, debt, and either healthy or LIQUIDATABLE with the repayment, what the liquidator receives, gas and the estimated profit. A market whose prices are not Fresh, a closed Stock Token market on a Saturday for instance, first prints liquidations blocked with the reason.

Commands

CommandWhat it does
scanevery position with debt, its health factor, and the estimated profit of the largest liquidation the close factor allows
liquidate <MARKET> <borrower>plan one liquidation; add --send to execute it, --shares N to repay less than the maximum
watchscan every --interval seconds (default 30) and liquidate what is due; add --send to execute, --min-profit (USD) to skip small ones

Nothing is sent without --send. With it, the script reads PRIVATE_KEY, sizes the liquidation to min(maxLiquidatableShares, the wallet's USDG balance) — stepping down to a size that leaves $5 behind if a smaller partial would otherwise revert as dust — approves exactly that amount (plus a 1% buffer for interest) to the market's pool, simulates the call from your address, and only then sends it. When it repays the whole permitted maximum it passes type(uint256).max, so the router caps the request at the maximum as of the block the transaction lands in. Whatever part of the buffer the pool does not use stays approved to that pool, a few USDG, until the next approval replaces it.

Configuration

All environment variables are optional, except PRIVATE_KEY with --send.

VariableDefault
PRIVATE_KEYthe liquidator wallet. Use a wallet that only holds what it needs
RPC_URLthe public RPCstrongly recommended: the public endpoint is rate-limited and answers bursts with 429 or a Cloudflare challenge. Free dRPC refuses the eth_getLogs ranges DISCOVERY=logs needs
DISCOVERYindexerlogs rebuilds the borrower list from events instead of asking the indexer
LOG_CHUNK50000blocks per eth_getLogs call with DISCOVERY=logs
GAS_LIMIT600000gas units assumed when estimating profit
DEBT_BUFFER_BPS100extra allowance over the estimated repayment

Addresses are not in the script. It reads them from /deployments/mainnet.json, which this site regenerates on every Paboxo deployment. Your bot keeps working across a redeploy without an update.

If the indexer is not answering, run with DISCOVERY=logs: the script then rebuilds the borrower list from events since the deployment block.

How it works

If you would rather write your own bot, these are the five steps the script takes.

1. Find borrowers

There is no on-chain list of borrowers. Either ask the indexer for every position that owes something:

query ($after: String) {
  positions(where: { borrowShares_gt: "0" }, limit: 500, after: $after) {
    pageInfo { hasNextPage endCursor }
    items { market user }   # market = the router address
  }
  _meta { status }          # the block the indexer has reached: refuse a stale list
}

or rebuild the list from events, starting at deployBlock: PositionCreated(lendingPool, lendingPoolRouter, user, position) on PaboxoEmitter and BorrowDebt(user, …) on each LendingPool. A stopped indexer still answers, with an old list, so check _meta.status against the chain head before trusting it.

2. Check health

IsHealthy.checkLiquidatable(address user, address router)
  returns (bool liquidatable, uint256 borrowValue, uint256 maxCollateralValue, uint256 bonus)

borrowValue and maxCollateralValue are USD with 18 decimals; maxCollateralValue is already multiplied by the liquidation threshold, so the health factor is maxCollateralValue / borrowValue. bonus is a WAD (0.08e18 = 8%). The borrower's debt in shares is LendingPoolRouter.userBorrowShares(user), and the most one liquidation may repay is

IsHealthy.maxLiquidatableShares(address user, address router) returns (uint256 shares)

It is an upper bound for the current block: the router accrues interest before it applies the cap.

3. Check the market is open

A liquidation reverts, whoever the borrower is, when:

  • the router or pool is paused (paused() on either);
  • the router requires fresh prices (requireFreshForLiquidation(), on by default) and TokenDataStream.priceStatus(token) is not 0 (Fresh) for the collateral or for USDG. Stock Token feeds report 1 (MarketClosed) from Friday's close until the first price after the reopen; see market hours;
  • the Stock Token issuer has paused transfers (paused() on the collateral token).

Check these first and you skip a reverted transaction. Expect a queue of liquidations in the first minutes after the market reopens.

4. Size and price it

Repay min(maxLiquidatableShares, what you can afford) borrow shares. The pool charges ceil(shares × totalBorrowAssets / totalBorrowShares) USDG and gives you the fraction repaidValue × (1 + bonus) / collateralValue of every token in the borrower's Position. When the Position cannot cover repaid × (1 + bonus), you take all of it and repay only what it is worth; the rest becomes bad debt. A partial below the maximum that would leave less than $5 of debt or collateral reverts MustNotLeaveDust. liquidationOutcome() in the script is a line-for-line copy of the router's arithmetic, close factor included, tested against the contract's own test vectors.

Interest accrues between your read and your transaction, so approve a little more than the estimate.

5. Send it

USDG.approve(lendingPool, amount);                       // exact amount, not unlimited
LendingPool.liquidate(address borrower, uint256 repayShares);   // value: 0

repayShares above what the borrower owes, or above the close factor, is capped, so type(uint256).max repays exactly the permitted maximum — the whole debt only at a health factor of 0.95 or below. Robinhood Chain does not charge a priority fee: send with maxPriorityFeePerGas = 0. Simulate from your own address first; the reverts are named (PriceNotFresh, CollateralTransfersPaused, EnforcedPause, AssetNotLiquidatable, MustNotLeaveDust), so a failed simulation tells you why.

The contract emits LiquidationExecuted(borrower, liquidator, repaidAssets, repaidShares, toLiquidatorWad, toProtocolWad, badDebt) on the pool.

Before you run it with money

  • Dry-run first. Run scan and watch without --send for a while and compare what they would have done with what happened on chain.
  • You compete. Paboxo's own liquidator and anyone else running this script read the same chain. The first transaction to land wins; the others revert cheaply in simulation.
  • You hold the collateral afterwards. Price moves between the liquidation and your sale are your risk. Selling right away on a DEX means paying its slippage, which on a thin pool can be most of a 6% bonus.
  • The script is a starter. It does not sell collateral, retry with higher gas, run several wallets, or alert you. It is MIT-licensed: copy it, change it, run it commercially.

On this page