> For the complete documentation index, see [llms.txt](https://docs.kairosswap.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kairosswap.com/dev-docs/liquidation-bot.md).

# 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.

{% hint style="info" %}
**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.
{% endhint %}

### 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:

```solidity
// Buyer is the owing side:
isBuyerLiquidatable = netObligation >= swap.collateralBalance;

// Pool is the owing side (triggers slightly early so the bounty is always covered):
liquidatorReward   = (poolCollateralBacking * market.liquidationIncentive) / WAD;
isPoolLiquidatable = netObligation >= (poolCollateralBacking - liquidatorReward);
```

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`:

```solidity
// Calculated at entry and held by the contract, separate from collateralBalance
uint256 liquidationBounty = (requiredBuyerCollateral * market.liquidationIncentive) / WAD;
uint256 totalBuyerRequired = requiredBuyerCollateral + protocolFee + liquidationBounty;
```

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:

```solidity
liquidatorReward = (poolCollateralBacking * market.liquidationIncentive) / WAD;
```

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.

{% hint style="info" %}
**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.
{% endhint %}

#### 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`:

```solidity
if (liquidatorTransfer > 0) {
    IERC20(swapToken).safeTransfer(liquidator, liquidatorTransfer);
}
```

#### Events to index

Two events fire on every liquidation:

```solidity
event SwapLiquidated(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed liquidatedParty, // buyer address if buyer-side, else address(0)
    bool buyerLiquidated,
    bool poolLiquidated,
    uint256 collateralTransferred,
    address liquidator               // = msg.sender
);

event SwapClosed(
    bytes32 indexed marketId,
    bytes32 indexed swapId,
    address indexed onBehalfOf,
    address caller,
    uint8 closureType,               // 2 = liquidated
    /* ...settlement amounts... */
    int256 floatingRate
);
```

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**.

```
┌───────────┐   ┌──────────┐   ┌───────────────┐   ┌────────┐
│ Discover  │──▶│  Filter  │──▶│  Confirm via  │──▶│ Submit │
│ open swaps│   │ off-chain│   │ eth_call sim  │   │  tx    │
└───────────┘   └──────────┘   └───────────────┘   └────────┘
```

{% hint style="warning" %}
**There is no public onchain `isLiquidatable` view.** `Utils.isSwapLiquidatable` is a library function over storage references — it is not externally callable, and SwapCore does not re-expose it. The robust way to confirm eligibility is to **simulate** `liquidateSwap` with `eth_call`: if the position is not liquidatable, it reverts with `E507`. Use off-chain math only to *cheaply pre-filter* candidates before simulating.
{% endhint %}

#### 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:

```graphql
query OpenSwaps {
  swaps(where: { status: "open" }, limit: 1000) {
    items {
      swapId
      marketId
      rateType
      entryTimestamp
      swapTerm
      buyerCollateral
      poolCollateralBacking
      liquidationBounty
      baseRate
      entryFloatingIndex
    }
  }
}
```

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

```
getExpiryQueueState(marketId) -> (length, pointer)
getExpiryQueueEntry(marketId, index) -> swapId
```

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.

```ts
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const swapCore = { address: SWAPCORE_ADDRESS, abi: SWAPCORE_ABI } as const;

async function isLiquidatableNow(swapId: `0x${string}`): Promise<boolean> {
  try {
    await publicClient.simulateContract({
      ...swapCore,
      functionName: "liquidateSwap",
      args: [swapId],
      account, // the bot's address
    });
    return true; // simulation succeeded -> liquidatable
  } catch (err) {
    // Reverts with E507 when not liquidatable; treat any revert as "skip"
    return false;
  }
}
```

#### Step 4 — Submit

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

```ts
async function liquidate(swapId: `0x${string}`) {
  const { request } = await publicClient.simulateContract({
    ...swapCore,
    functionName: "liquidateSwap",
    args: [swapId],
    account,
  });
  const hash = await walletClient.writeContract(request);
  await publicClient.waitForTransactionReceipt({ hash });
  return hash;
}
```

#### Putting it together

```ts
async function tick() {
  const open = await fetchOpenSwaps();          // Step 1 (indexer GraphQL)
  const now = Math.floor(Date.now() / 1000);

  const candidates = open
    .filter(s => now < s.entryTimestamp + s.swapTerm) // Step 2: drop expired
    .filter(estimateUnderwater);                       // Step 2: cheap ranking

  for (const s of candidates) {
    if (await isLiquidatableNow(s.swapId)) {           // Step 3: confirm
      try {
        const hash = await liquidate(s.swapId);        // Step 4: submit
        console.log(`liquidated ${s.swapId} -> ${hash}`);
      } catch {
        // Lost the race (someone else liquidated, or rate moved back). Move on.
      }
    }
  }
}

setInterval(tick, 12_000); // roughly one block on most L2s; tune to your chain
```

#### Operational notes

{% hint style="info" %}
**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.
{% endhint %}

* **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.

{% hint style="success" %}
**Net effect:** the worst case for a Kairos liquidator is a reverted transaction (you lose only gas, and the pre-send simulation prevents most of those). This makes the role well suited to a lightweight keeper that optimizes purely for latency and gas, without requiring a treasury, hedging, or inventory management.
{% endhint %}

#### 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                                      |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.kairosswap.com/dev-docs/liquidation-bot.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
