Velocity ProtocolDevelopers
DevelopersTrading automationKeeper Bots

Tutorial: Liquidation Bot

A liquidation on Velocity is not a forced sale into the book. It is a liability transfer: the liquidator takes the failing account's position onto its own books and is paid for doing so with the liquidatee's collateral at a discount. That single fact is what makes this bot different from the matching and trigger bots. Those two are paid a flat fee for pressing a button. This one is paid in risk it has to carry and then unwind.

Two consequences follow, and both shape how the bot is written. A liquidator needs collateral, because the inherited position has to fit inside the liquidator's own initial margin requirement. It also needs an exit, because the discount is only profit once the position is closed at a price near the one it was inherited at. A liquidator without a derisk path is a directional trader that did not choose its direction.

The mechanism, the discount, the fee split, and the bankruptcy path that follows a failed liquidation are in Liquidations. This page covers running the reference bot.

The instruction set

Velocity does not have one liquidation instruction. Which one applies depends on what the failing account holds:

InstructionWhen it applies
liquidate_perpThe account is short of maintenance margin and holds a perp position. The liquidator inherits the position.
liquidate_perp_with_fillSame case, but routed through a fill rather than a transfer to the liquidator. Blocked when the exchange's FillPaused bit is set.
liquidate_spotA spot borrow against a spot deposit. The liquidator takes on the borrow and receives the deposit.
liquidate_spot_with_swap_begin and _endA spot liquidation that routes through an external swap inside one transaction, so the liquidator does not have to hold the asset.
liquidate_borrow_for_perp_pnlThe account has a spot borrow and positive unsettled perp P&L. The liquidator takes on the borrow and receives the P&L.
liquidate_perp_pnl_for_depositThe account has negative unsettled perp P&L and a spot deposit. The liquidator takes on the negative P&L and receives the deposit.

All of them are permissionless: anyone who can fund the resulting position can call them. All of them are also gated on the exchange's LiqPaused bit being clear, and liquidate_perp_with_fill additionally on FillPaused. See Guard rails for the pause matrix.

liquidate_perp_with_fill runs the fill in liquidation mode, and that suppresses the post-fill margin check on the liquidatee entirely. An ordinary fill re-checks the taker after the fill, against the maintenance requirement when the order reduces their position and the fill-weight requirement when it does not. A liquidation fill runs neither, so it does not revert because the account it is unwinding is still short of margin. Do not size the fill against a margin check that will not run.

Running the reference bot

The implementations are src/bots/liquidator.ts and src/bots/liquidatorDerisk.ts in apps/keeper-bots-v2, inside the velocity-v1 monorepo, which is not public yet. Between them they handle scanning accounts, checking margin, choosing an instruction, and unwinding what the liquidation left behind.

Follow the base setup in Trading Automation first. On top of it, this bot needs the quote asset deposited as collateral before it starts, not merely held in the wallet. --force-deposit moves it.

Start it with bun run dev:liquidator, or by adding liquidator to enabledBots in the config file.

Configuration that matters

The botConfigs.liquidator block is where the risk decisions live. The ones worth setting deliberately:

perpSubAccountConfig and spotSubAccountConfig map each market index to the subaccount that is willing to inherit risk in it. This is how an unwanted market's position stays off the subaccount holding working capital. Leaving them null watches every market on global.subaccounts[0].

maxPositionTakeoverPctOfCollateral caps how much of the bot's collateral a single liquidation may commit, as a fraction (0.5 is 50%). It is the difference between missing an opportunity and being unable to take the next one.

maxSlippageBps sets the worst price the derisk leg will accept, and is used both to compute the auction end price on the unwind order and as the slippage limit on swaps. deriskAuctionDurationMs sets how long that unwind auction runs, in wall-clock milliseconds.

minDepositToLiq, per spot market in lamports, skips positions too small to be worth the transaction fee. spotDustValueThreshold does the matching job on the other side, sweeping inherited spot balances below a dollar value back to the authority wallet instead of leaving them scattered across subaccounts.

disableAutoDerisking turns off the unwind entirely, leaving every inherited position open. Set it only if something else in the stack is managing that exposure.

Where to spend effort

The reference bot rechecks accounts on a polling loop and evaluates them in no particular order. Two changes close most of the gap between it and a liquidator that wins races:

  • Sort by distance to liquidation. Keep accounts in a structure ordered by the price at which each becomes liquidatable, so the next candidate is the head of the list rather than the result of a full scan.
  • Wake on price, not on a timer. Watch the oracle and run the margin check when the price moves through a level that matters, instead of every interval regardless.

Come talk about better implementations in #research-and-dev-chat on Discord. Stronger liquidation engines protect the insurance fund and every depositor behind it.