Liquidation Bot

How to build a liquidation bot

Building a Liquidation Bot on Kairos

Kairos liquidations are unusually simple to keep. There is no flash loan, no collateral auction, no debt to assume, and no token you are forced to hold. A liquidator is a pure keeper: it spots a position whose margin has run out, calls one function, and walks away with a bounty that was escrowed up front. This guide explains the mechanism, then walks through building a bot end to end.

TLDR — Call liquidateSwap(swapId) on an undercollateralized swap. You pay gas; the protocol pays you a prefunded bounty in the market's settlement token (e.g. USDC). You never take on the position, inventory, or price risk.

1. How liquidation works

What a swap looks like

Each Kairos market is an onchain interest rate swap. A buyer posts collateral to take a fixed-vs-floating position for a fixed term; the LP pool backs the other side. As the reference rate moves, one side accrues an obligation to the other. That obligation is settled from collateral both sides posted at entry.

A position can close three ways:

Closure

How

closureType

Expiry

Anyone calls makePayment after the term ends

0

Early exit

Buyer exits before expiry (if the market allows it)

1

Liquidation

Anyone calls liquidateSwap while the position is underwater

2

Liquidation exists for the case where the rate moves so far against one side, before expiry, that its posted collateral can no longer cover what it owes. Rather than let the position drift further underwater, anyone may force-settle it early.

When is a swap liquidatable?

A position is liquidatable when all of these hold:

  1. It is not settled. Closed positions are skipped.

  2. It has not expired. If block.timestamp >= entryTimestamp + swapTerm, it is not liquidatable — expired positions are settled normally via makePayment, not liquidated.

  3. Accrued obligation has consumed the owing side's collateral.

The third condition uses accrued payments only — the mark-to-market obligation up to now, derived from the move in the reference-rate index since entry. Projected future payments are not counted.

The threshold differs by which side owes:

In plain terms: the buyer becomes liquidatable once its accrued obligation reaches the collateral it posted. The pool side becomes liquidatable just before its backing is fully consumed, so there is always enough left to pay the liquidator.

Where the bounty comes from

The liquidator's reward is always a liquidationIncentive (a WAD percentage set per market) slice of the owing side's collateral — but the source depends on which side is liquidated.

Buyer-side liquidation — paid from the buyer's prefunded bounty. When the swap is created, the buyer prefunds a bounty inside buySwap:

It is stored on the position as swap.liquidationBounty and kept out of the bucket accounting so it cannot be confused with margin. If the buyer is liquidated, this prefunded amount is paid to whoever called liquidateSwap.

Pool-side liquidation — paid from the pool's collateral. When the pool is the owing side, the buyer's prefunded bounty is not used — it is returned to the buyer in full. Instead the reward is computed fresh from the pool's backing at liquidation time and carved out of it:

In both cases the reward is already sitting in the contract before your bot shows up — either as the buyer's prefunded bounty or as part of the pool's backing. You are never fronting it.

Why it matters for a bot: your expected payout is liquidationIncentive × (collateral of whichever side is underwater). For a buyer-side liquidation that is the buyer's prefunded liquidationBounty; for a pool-side liquidation it is a slice of poolCollateralBacking. Size your gas-vs-reward check against the correct side.

What happens on liquidation

liquidateSwap updates the oracle index, re-checks eligibility, force-settles the position, and pays out — strictly checks-effects-interactions, with external transfers last.

If the buyer is liquidated:

  • Liquidator receives the prefunded swap.liquidationBounty.

  • The buyer's entire collateralBalance is credited to the pool.

  • The buyer receives nothing back.

If the pool is liquidated:

  • Liquidator receives poolCollateralBacking * liquidationIncentive / WAD.

  • The buyer receives the remaining pool backing, plus their own original collateral, plus their own prefunded bounty back.

Either way the position is marked settled, removed from its bucket, and the bounty is sent to msg.sender with a direct safeTransfer in the market's swapToken:

Events to index

Two events fire on every liquidation:

A bot can confirm its own fills by watching SwapLiquidated for its address, or filter SwapClosed on closureType == 2.

2. How to build a liquidation bot

A liquidation bot is a loop with four stages: discover open positions, filter to likely candidates, confirm onchain, and submit.

Step 1 — Discover open positions

There is no onchain array of all swap IDs. Three discovery sources, easiest first:

A. The indexer (recommended). Kairos runs a Ponder indexer exposing a GraphQL API (default dev endpoint https://idxdev.kairosswap.com/graphql). The swaps table carries a status field ("open" | "expired" | "earlyExit" | "liquidated") and every entry field you need for the math:

B. The onchain expiry queue. Each market keeps an append-only queue of swap IDs with a pointer to the earliest unsettled one:

Walk [pointer, length) to enumerate live swaps without the indexer. (The queue is ordered by entry for expiry settlement, not pruned of unexpired-but-liquidatable entries, so you still filter yourself.)

C. SwapCreated event logs. Backfill historical swaps directly from chain if you do not want to depend on the indexer.

Step 2 — Pre-filter off-chain

For each open swap, drop anything that cannot be liquidated, cheaply:

  • Expired? If now >= entryTimestamp + swapTerm, skip — that is a makePayment, not a liquidation.

  • Estimate the obligation. Read the live reference index (getFreshIndex(marketId)), derive the floating rate from entryFloatingIndex versus the live index over the elapsed time, and compare accrued fixed vs floating legs against the owing side's collateral. Treat this only as a ranking heuristic — the contract's math is authoritative.

A lightweight proxy for "how close to the edge" is getSwapNetAmount(swapId), which returns the current net settlement amount and direction.

Step 3 — Confirm with a simulation

For each surviving candidate, simulate the real call. If it would succeed, it is liquidatable right now.

Step 4 — Submit

When the simulation passes, send the transaction. The call takes only the swapId — no amount, no token, no approval.

Putting it together

Operational notes

Liquidation is permissionless and competitive. liquidateSwap has no whitelist — anyone can call it. Expect other keepers. Win on latency (poll frequently, simulate fast) and gas strategy, not on privileged access.

  • Idempotency / races. Once a swap is liquidated it is settled; a second call reverts. Always rely on the pre-send simulateContract so you do not burn gas on a position someone else just took.

  • Keep the index fresh. liquidateSwap updates the oracle index itself before checking, so you do not have to. But your off-chain estimate uses the last snapshot — a position can cross the threshold the instant a new index lands. Re-simulate close to send time.

  • Watch both sides. A market has a buyer side and a pool side, and either can be the one that gets liquidated. Your math must handle rateType (BUY_FIXED = buyer pays fixed; BUY_FLOATING = buyer receives fixed), which flips who owes.

  • Gas vs bounty. The bounty is liquidationIncentive (a WAD percentage set per market) of the owing side's collateral. Before sending, confirm the bounty exceeds your gas cost — small positions on expensive chains may not be worth it.

3. The benefits — and the absence of collateral price risk

Kairos's liquidation model is deliberately a trigger, not a takeover. That removes the risks that make liquidation bots elsewhere capital-intensive and dangerous.

You provide gas, nothing else

There is no transferFrom anywhere in the liquidation path. You do not deposit collateral, you do not need token approvals, and you do not need a flash loan to fund a repayment. The bounty is already escrowed inside the contract — you call one function and receive it. The entire capital requirement is the gas for the transaction.

You never hold the position or any inventory

In an AMM or lending-market liquidation you typically buy the collateral, assume the debt, or take the seized asset onto your balance sheet — and then you have to sell it, exposing you to slippage and price moves in the window before you do. On Kairos you become a counterparty to nothing. liquidateSwap force-settles the position in place: collateral is routed between the existing buyer and pool, the position is marked settled, and you receive a fixed bounty. You hold no swap, no leg, no seized collateral.

No collateral price risk

Because you never acquire an asset, there is no asset to mark against the market. Your payout is the prefunded bounty, denominated in the market's swapToken — typically a stablecoin like USDC. The only "price exposure" you ever have is whatever you choose by holding that token afterward. There is:

  • No volatile collateral to offload before the price moves against you.

  • No debt to assume and refinance.

  • No flash-loan leg that can fail and strand you.

  • No inventory carried between blocks.

Why the protocol wants you there

Liquidations keep the protocol solvent: they close out underwater positions before the obligation exceeds posted collateral, protecting the counterparty (buyer or LP pool) from being left with an unbacked claim. The prefunded bounty exists precisely to make sure that work is always profitable to perform and always available to anyone — no permissions, no capital, no price risk.


Quick reference

Entry point

liquidateSwap(bytes32 swapId) — permissionless, gas only

Eligibility

not settled, not expired, accrued obligation ≥ owing side's collateral

Reward

prefunded liquidationBounty (buyer-side) or poolBacking × incentive (pool-side), paid in swapToken

Confirm before sending

eth_call simulate liquidateSwap; reverts E507 if not liquidatable

Discover positions

indexer GraphQL swaps(status:"open"), or onchain expiry queue, or SwapCreated logs

Events

SwapLiquidated, SwapClosed (closureType=2)

Risk

gas on a reverted tx only — no collateral, inventory, or price risk

Last updated