> ## Documentation Index
> Fetch the complete documentation index at: https://kamino.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Liquidation Bot with KSwap

> Build a bot that monitors obligations, identifies profitable pairs, swaps into debt tokens via KSwap, and executes liquidations

Liquidations on Kamino are permissionless. Any wallet can liquidate an unhealthy obligation and earn a bonus. Bots continuously monitor all obligations and execute when one crosses its health limit.

## Scanning for Unhealthy Positions

The first step is loading every obligation in a market and checking which ones have breached their health limit.

<Tabs>
  <Tab title="TypeScript">
    <Steps>
      <Step>
        ### Import Dependencies

        ```typescript theme={null}
        import {
          createSolanaRpc,
          createSolanaRpcSubscriptions,
          createKeyPairSignerFromBytes,
          sendAndConfirmTransactionFactory,
          address,
        } from '@solana/kit';
        import {
          KaminoMarket,
          KaminoObligation,
          DEFAULT_RECENT_SLOT_DURATION_MS,
        } from '@kamino-finance/klend-sdk';
        ```
      </Step>

      <Step>
        ### Set Up RPC and Load Market

        ```typescript theme={null}
        const RPC_ENDPOINT = 'YOUR_RPC_URL';
        const WS_ENDPOINT = RPC_ENDPOINT.replace('https://', 'wss://');
        const SOL_BTC_MARKET = '7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF';

        const rpc = createSolanaRpc(RPC_ENDPOINT);
        const rpcSubscriptions = createSolanaRpcSubscriptions(WS_ENDPOINT);
        const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

        const keypairBytes = new Uint8Array(JSON.parse(process.env.LENDING_USER_SECRET_KEY!));
        const liquidator = await createKeyPairSignerFromBytes(keypairBytes);

        const market = await KaminoMarket.load(
          rpc, address(SOL_BTC_MARKET), DEFAULT_RECENT_SLOT_DURATION_MS,
        );
        ```

        <Note>
          Kamino operates multiple markets (SOL/BTC, JLP, Altcoins, Prime). A production bot should scan all of them. See [Markets](/docs/products/borrow/markets) for the full list.
        </Note>
      </Step>

      <Step>
        ### Scan Obligations in Batches

        Scan obligations in batches of 100 to avoid out-of-memory on large markets. Use the on-chain LTV methods, not `refreshedStats.loanToValue`, to match the program's liquidation check.

        ```typescript theme={null}
        // Scanning 100k+ obligations in batches takes several minutes.
        const currentSlot = await rpc.getSlot().send();
        const unhealthy: KaminoObligation[] = [];
        let totalScanned = 0;

        for await (const batch of market!.batchGetAllObligationsForMarket(currentSlot)) {
          totalScanned += batch.length;
          batch.forEach((ob) => {
            const ltv = ob.loanToValue();
            const liqLtv = ob.liquidationLtv();
            if (ltv.gte(liqLtv) && ob.refreshedStats.userTotalBorrow.gt(0)) {
              unhealthy.push(ob);
            }
          });
        }

        console.log(`Found ${unhealthy.length} liquidatable out of ${totalScanned} total`);
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Rust">
    <Steps>
      <Step>
        ### Add Dependencies

        ```toml theme={null}
        [dependencies]
        klend-interface = { version = "0.6.0", features = ["solana-account"] }
        solana-pubkey = "2.1"
        solana-instruction = "2.1"
        solana-sdk = "~2.3"
        solana-client = "~2.3"
        solana-account = "2.1"
        solana-account-decoder-client-types = "~2.3"
        spl-token = "7"
        spl-associated-token-account = "6"
        reqwest = { version = "~0.12", features = ["blocking", "json"] }
        serde = { version = "1", features = ["derive"] }
        serde_json = "1"
        bincode = "1"
        base64 = "0.22"
        ```

        <Note>
          `klend-interface` is a lightweight Rust instruction builder that creates `Vec<Instruction>` with required refresh instructions prepended automatically.
        </Note>
      </Step>

      <Step>
        ### Set Up RPC Client

        ```rust theme={null}
        use solana_client::rpc_client::RpcClient;
        use solana_pubkey::Pubkey;
        use std::str::FromStr;

        let rpc_client = RpcClient::new("YOUR_RPC_URL");
        let market = Pubkey::from_str("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF")?;
        ```

        <Warning>
          Scanning obligations requires `getProgramAccounts`, which returns thousands of accounts per market. The public Solana RPC will reject these calls. Use a private RPC provider (Helius, Triton, QuickNode, etc.).
        </Warning>
      </Step>

      <Step>
        ### Fetch All Obligations

        Fetch every obligation in the market using `get_program_accounts` with filters for the discriminator, lending market, and account size.

        ```rust theme={null}
        use klend_interface::{
            state::{Obligation, SplDiscriminate},
            KLEND_PROGRAM_ID,
        };
        use solana_account::ReadableAccount;
        use solana_account_decoder_client_types::UiAccountEncoding;
        use solana_client::{
            rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
            rpc_filter::{Memcmp, RpcFilterType},
        };

        const OBLIGATION_ACCOUNT_SIZE: u64 = 8 + std::mem::size_of::<Obligation>() as u64;

        let filters = vec![
            RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
                0,
                Obligation::SPL_DISCRIMINATOR_SLICE.to_vec(),
            )),
            RpcFilterType::Memcmp(Memcmp::new_raw_bytes(32, market.to_bytes().to_vec())),
            RpcFilterType::DataSize(OBLIGATION_ACCOUNT_SIZE),
        ];

        let config = RpcProgramAccountsConfig {
            filters: Some(filters),
            account_config: RpcAccountInfoConfig {
                encoding: Some(UiAccountEncoding::Base64Zstd),
                ..Default::default()
            },
            ..Default::default()
        };

        let accounts = rpc_client.get_program_accounts_with_config(&KLEND_PROGRAM_ID, config)?;
        ```

        <Warning>
          Obligation accounts exceed the 128-byte limit for base58 responses. Use `Base64Zstd` encoding or the RPC will silently return zero results.
        </Warning>
      </Step>

      <Step>
        ### Filter to Liquidatable Positions

        Parse each obligation and check `is_liquidatable()`, which returns true when borrow-factor-adjusted debt exceeds the unhealthy borrow value.

        ```rust theme={null}
        use klend_interface::Fraction;

        let mut liquidatable = Vec::new();

        for (pubkey, account) in &accounts {
            let obligation = klend_interface::from_account_data::<Obligation>(account.data())?;

            if obligation.has_debt == 0 {
                continue;
            }

            if obligation.is_liquidatable() {
                let debt: f64 = Fraction::from_bits(
                    obligation.borrowed_assets_market_value()
                ).to_num();

                println!("Liquidatable: {pubkey} — debt: ${debt:.2}");
                liquidatable.push((*pubkey, obligation));
            }
        }

        println!("Initial candidates (stale prices): {}", liquidatable.len());
        ```
      </Step>

      <Step>
        ### Verify with Fresh Prices

        On-chain obligation values are only updated when `RefreshObligation` is called. Fetch fresh oracle prices from the Scope REST API and re-verify each candidate to avoid wasting transaction fees on false positives.

        ```rust theme={null}
        use klend_interface::state::{Reserve, from_account_data};
        use serde::Deserialize;
        use std::collections::{HashMap, HashSet};

        #[derive(Deserialize)]
        struct OraclePrice {
            mint: String,
            price: String,
        }

        // Collect all reserve pubkeys from candidates
        let reserve_pubkeys: Vec<Pubkey> = liquidatable
            .iter()
            .flat_map(|(_, obl)| {
                obl.deposits.iter().map(|d| d.deposit_reserve)
                    .chain(obl.borrows.iter().map(|b| b.borrow_reserve))
                    .filter(|pk| *pk != Pubkey::default())
            })
            .collect::<HashSet<_>>()
            .into_iter()
            .collect();

        // Fetch reserves to map mints to prices and get borrow factors
        let reserve_accounts = rpc_client.get_multiple_accounts(&reserve_pubkeys)?;
        let reserve_map: HashMap<Pubkey, Reserve> = reserve_pubkeys
            .iter()
            .zip(reserve_accounts.iter())
            .filter_map(|(pk, acc)| {
                acc.as_ref().and_then(|a| {
                    from_account_data::<Reserve>(&a.data).ok().map(|r| (*pk, *r))
                })
            })
            .collect();

        // Fetch fresh prices from the Scope REST API
        let fresh_prices: HashMap<String, f64> = reqwest::blocking::get(
            "https://api.kamino.finance/oracles/prices",
        )
            .ok()
            .and_then(|r| r.json::<Vec<OraclePrice>>().ok())
            .map(|prices| prices.into_iter()
                .filter_map(|p| p.price.parse::<f64>().ok().map(|v| (p.mint, v)))
                .collect())
            .unwrap_or_default();

        let mut verified = Vec::new();

        for (pubkey, obligation) in &liquidatable {
            // Re-price deposits
            let mut fresh_deposited = 0.0f64;
            let mut fresh_unhealthy = 0.0f64;
            for deposit in &obligation.deposits {
                if deposit.deposit_reserve == Pubkey::default() { continue; }
                let reserve = match reserve_map.get(&deposit.deposit_reserve) {
                    Some(r) => r,
                    None => continue,
                };
                let mint = reserve.liquidity.mint_pubkey;
                let stale_price: f64 = Fraction::from_bits(
                    u128::from(reserve.liquidity.market_price_sf)
                ).to_num();
                let price = fresh_prices.get(&mint.to_string()).copied().unwrap_or(stale_price);
                let amount: f64 = Fraction::from_bits(deposit.deposited_amount.into()).to_num();
                let value = amount / 10f64.powi(reserve.liquidity.mint_decimals as i32) * price;
                fresh_deposited += value;
                fresh_unhealthy += value * reserve.liquidation_threshold_pct() as f64 / 100.0;
            }

            // Re-price borrows
            let mut fresh_adjusted_debt = 0.0f64;
            for borrow in &obligation.borrows {
                if borrow.borrow_reserve == Pubkey::default() { continue; }
                let reserve = match reserve_map.get(&borrow.borrow_reserve) {
                    Some(r) => r,
                    None => continue,
                };
                let mint = reserve.liquidity.mint_pubkey;
                let stale_price: f64 = Fraction::from_bits(
                    u128::from(reserve.liquidity.market_price_sf)
                ).to_num();
                let price = fresh_prices.get(&mint.to_string()).copied().unwrap_or(stale_price);
                let amount: f64 = Fraction::from_bits(borrow.borrowed_amount()).to_num();
                let value = amount / 10f64.powi(reserve.liquidity.mint_decimals as i32) * price;
                fresh_adjusted_debt += value * reserve.borrow_factor_pct() as f64 / 100.0;
            }

            // Still liquidatable at fresh prices?
            if fresh_adjusted_debt > fresh_unhealthy {
                verified.push((*pubkey, *obligation));
            }
        }

        println!("Verified {} out of {} candidates with fresh prices", verified.len(), liquidatable.len());
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Picking the Most Profitable Pair

An obligation can have multiple collateral deposits and multiple borrows. The liquidator must choose one debt token to repay and one collateral token to seize. The program enforces priority rules: target the lowest-threshold collateral and the highest-borrow-factor debt first.

<Tabs>
  <Tab title="TypeScript">
    <Steps>
      <Step>
        ### Load the Obligation

        ```typescript theme={null}
        import { KaminoReserve } from '@kamino-finance/klend-sdk';
        import { Address } from '@solana/kit';

        const obligation = (await market!.getObligationByAddress(
          address('<OBLIGATION_PUBKEY>')
        ))!;

        const resolveReserve = (market: KaminoMarket, reserveAddr: Address) =>
          market.getReserveByAddress(reserveAddr) as KaminoReserve | undefined;
        ```
      </Step>

      <Step>
        ### Sort by Priority

        Sort deposits by `liquidationThresholdPct` ascending (weakest collateral first) and borrows by `borrowFactorPct` descending (riskiest debt first).

        ```typescript theme={null}
        const deposits = obligation.getDeposits();
        const borrows = obligation.getBorrows();

        const sortedDeposits = deposits.sort((a, b) => {
          const reserveA = resolveReserve(market!, a.reserveAddress)!;
          const reserveB = resolveReserve(market!, b.reserveAddress)!;
          return reserveA.state.config.liquidationThresholdPct
               - reserveB.state.config.liquidationThresholdPct;
        });

        const sortedBorrows = borrows.sort((a, b) => {
          const reserveA = resolveReserve(market!, a.reserveAddress)!;
          const reserveB = resolveReserve(market!, b.reserveAddress)!;
          return Number(reserveB.state.config.borrowFactorPct)
               - Number(reserveA.state.config.borrowFactorPct);
        });
        ```
      </Step>

      <Step>
        ### Select the Target Pair

        The first entry in each sorted list is the priority target. Reserves with `loanToValuePct == 0` are deposit-only and cannot be seized.

        ```typescript theme={null}
        const targetCollateral = sortedDeposits.find((d) => {
          const reserve = resolveReserve(market!, d.reserveAddress);
          return reserve && reserve.state.config.loanToValuePct > 0;
        });

        const targetDebt = sortedBorrows[0];

        if (!targetCollateral || !targetDebt) {
          console.log('No valid liquidation pair found');
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Rust">
    <Steps>
      <Step>
        ### Fetch Reserve Data

        Collect all unique reserve pubkeys from the obligation's deposits and borrows, then fetch and parse them in one RPC call.

        ```rust theme={null}
        use klend_interface::state::{Reserve, from_account_data};
        use std::collections::HashSet;

        let active_deposits: Vec<_> = obligation
            .deposits
            .iter()
            .filter(|d| d.deposit_reserve != Pubkey::default())
            .collect();

        let active_borrows: Vec<_> = obligation
            .borrows
            .iter()
            .filter(|b| b.borrow_reserve != Pubkey::default())
            .collect();

        let reserve_pubkeys: Vec<Pubkey> = active_deposits
            .iter()
            .map(|d| d.deposit_reserve)
            .chain(active_borrows.iter().map(|b| b.borrow_reserve))
            .collect::<HashSet<_>>()
            .into_iter()
            .collect();

        let reserve_accounts = rpc_client.get_multiple_accounts(&reserve_pubkeys)?;

        let parsed_reserves: Vec<(Pubkey, Reserve)> = reserve_pubkeys
            .iter()
            .zip(reserve_accounts.iter())
            .filter_map(|(pk, acc_opt)| {
                let acc = acc_opt.as_ref()?;
                let reserve = from_account_data::<Reserve>(&acc.data).ok()?;
                Some((*pk, *reserve))
            })
            .collect();
        ```
      </Step>

      <Step>
        ### Sort by Priority

        Sort deposits by `liquidation_threshold_pct` ascending (weakest collateral first) and borrows by `borrow_factor_pct` descending (riskiest debt first).

        ```rust theme={null}
        let mut deposit_pairs: Vec<(Pubkey, &Reserve)> = active_deposits
            .iter()
            .filter_map(|d| {
                parsed_reserves
                    .iter()
                    .find(|(pk, _)| *pk == d.deposit_reserve)
                    .map(|(pk, r)| (*pk, r))
            })
            .collect();

        deposit_pairs.sort_by_key(|(_, r)| r.liquidation_threshold_pct());

        let mut borrow_pairs: Vec<(Pubkey, &Reserve)> = active_borrows
            .iter()
            .filter_map(|b| {
                parsed_reserves
                    .iter()
                    .find(|(pk, _)| *pk == b.borrow_reserve)
                    .map(|(pk, r)| (*pk, r))
            })
            .collect();

        borrow_pairs.sort_by(|(_, a), (_, b)| b.borrow_factor_pct().cmp(&a.borrow_factor_pct()));
        ```
      </Step>

      <Step>
        ### Select the Target Pair

        The first entry in each sorted list is the priority target. Collateral reserves with `loan_to_value_pct == 0` are deposit-only and cannot be seized.

        ```rust theme={null}
        let withdraw_pair = deposit_pairs
            .iter()
            .find(|(_, r)| r.loan_to_value_pct() > 0);

        let repay_pair = borrow_pairs.first();

        match (withdraw_pair, repay_pair) {
            (Some((coll_pk, coll_reserve)), Some((debt_pk, debt_reserve))) => {
                println!("Seize collateral: {coll_pk} (mint: {})", coll_reserve.liquidity.mint_pubkey);
                println!("Repay debt: {debt_pk} (mint: {})", debt_reserve.liquidity.mint_pubkey);
            }
            _ => println!("No valid liquidation pair found"),
        }
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Executing the Liquidation

Once an unhealthy obligation and target pair have been identified, build and send the liquidation transaction.

<Tabs>
  <Tab title="TypeScript">
    <Steps>
      <Step>
        ### Import Transaction Utilities

        ```typescript theme={null}
        import {
          pipe,
          createTransactionMessage,
          setTransactionMessageFeePayerSigner,
          setTransactionMessageLifetimeUsingBlockhash,
          appendTransactionMessageInstructions,
          signTransactionMessageWithSigners,
          getSignatureFromTransaction,
          getTransactionDecoder,
          getCompiledTransactionMessageDecoder,
          decompileTransactionMessageFetchingLookupTables,
          addSignersToTransactionMessage,
        } from '@solana/kit';
        import { KaminoAction } from '@kamino-finance/klend-sdk';
        import BN from 'bn.js';
        import Decimal from 'decimal.js';
        ```
      </Step>

      <Step>
        ### Calculate Repay Amount

        Calculate the maximum repayable amount from the obligation's debt position and the market's close factor.

        ```typescript theme={null}
        const closeFactorPct = market!.state.liquidationMaxDebtCloseFactorPct;
        const debtReserve = resolveReserve(market!, targetDebt!.reserveAddress)!;
        const debtMintFactor = debtReserve.getMintFactor();
        const maxRepayLamports = targetDebt!.amount
          .mul(new Decimal(closeFactorPct))
          .div(new Decimal(100))
          .floor();
        const repayAmount = new BN(maxRepayLamports.toFixed(0));
        ```
      </Step>

      <Step>
        ### Swap into Debt Token via KSwap

        If the liquidator does not hold the debt token, swap into it using the KSwap REST API. The swap amount is converted from debt USD value to SOL lamports.

        ```typescript theme={null}
        const SOL_MINT = 'So11111111111111111111111111111111111111112';
        const KSWAP_API = 'https://api.kamino.finance';
        const GAS_RESERVE_LAMPORTS = 50_000_000; // 0.05 SOL reserved for tx fees

        const debtPrice = debtReserve.getOracleMarketPrice();
        const repayUsd = new Decimal(repayAmount.toString()).mul(debtPrice).div(debtMintFactor);

        // Fetch SOL price from oracle API (price is returned as a string)
        const solPriceRes = await fetch(`${KSWAP_API}/oracles/prices?mints=${SOL_MINT}`);
        if (!solPriceRes.ok) {
          throw new Error(`Oracle API error ${solPriceRes.status}: ${await solPriceRes.text()}`);
        }
        const solPriceData = await solPriceRes.json();
        const solPrice = parseFloat(solPriceData[0]?.price);

        if (!Number.isFinite(solPrice) || solPrice <= 0) {
          console.log('Invalid SOL price from oracle API, skipping swap');
        }

        // Convert debt USD to SOL lamports, cap by wallet balance minus gas reserve
        const solLamports = Math.ceil(repayUsd.div(solPrice).mul(1e9).toNumber());
        const walletBalance = await rpc.getBalance(liquidator.address).send();
        const availableLamports = Number(walletBalance.value) - GAS_RESERVE_LAMPORTS;
        const amountIn = Math.min(solLamports, Math.max(0, availableLamports));

        const swapParams = new URLSearchParams({
          tokenIn: SOL_MINT,
          tokenOut: targetDebt!.mintAddress.toString(),
          amountIn: amountIn.toString(),
          maxSlippageBps: '50',
          wallet: liquidator.address,
          wrapAndUnwrapSol: 'true',
        });

        const swapRes = await fetch(`${KSWAP_API}/kswap/swap/?${swapParams}`);

        if (!swapRes.ok) {
          const body = await swapRes.text();
          console.log(`KSwap API error ${swapRes.status}: ${body}`);
        }

        const swapData = swapRes.ok ? (await swapRes.json()).data : undefined;

        if (swapData?.transaction) {
          const swapTxBuffer = Buffer.from(swapData.transaction, 'base64');
          const swapMsgBytes = getTransactionDecoder().decode(swapTxBuffer).messageBytes;
          const swapCompiled = getCompiledTransactionMessageDecoder().decode(swapMsgBytes);

          const { value: swapBlockhash } = await rpc
            .getLatestBlockhash({ commitment: 'finalized' }).send();

          const signedSwap = await pipe(
            await decompileTransactionMessageFetchingLookupTables(swapCompiled, rpc),
            (tx) => setTransactionMessageLifetimeUsingBlockhash(swapBlockhash, tx),
            (tx) => setTransactionMessageFeePayerSigner(liquidator, tx),
            (tx) => addSignersToTransactionMessage([liquidator], tx),
            (tx) => signTransactionMessageWithSigners(tx),
          );

          await sendAndConfirm(signedSwap, { commitment: 'confirmed', skipPreflight: true });
          console.log('Swapped into debt token:', getSignatureFromTransaction(signedSwap));
        }
        ```

        <Note>
          KSwap is also available as the `@kamino-finance/kswap-sdk` TypeScript package for building swap instructions on the client.
        </Note>
      </Step>

      <Step>
        ### Calculate Slippage Protection

        Calculate the minimum collateral to receive based on the repay value, expected liquidation bonus, protocol fee, collateral price, and a 2% haircut.

        ```typescript theme={null}
        const MIN_RECEIVED_SLIPPAGE_BPS = 200; // 2%
        const collateralReserve = resolveReserve(market!, targetCollateral!.reserveAddress)!;
        const collateralPrice = collateralReserve.getOracleMarketPrice();
        const collateralMintFactor = collateralReserve.getMintFactor();
        const bonusBps = collateralReserve.state.config.maxLiquidationBonusBps;
        const protocolFeePct = collateralReserve.state.config.protocolLiquidationFeePct;

        const bonusMultiplier = new Decimal(1).plus(new Decimal(bonusBps).div(10_000));
        const afterProtocolFee = new Decimal(1).minus(new Decimal(protocolFeePct).div(100));
        const slippageMultiplier = new Decimal(1).minus(new Decimal(MIN_RECEIVED_SLIPPAGE_BPS).div(10_000));

        const expectedCollateral = repayUsd
          .mul(bonusMultiplier)
          .mul(afterProtocolFee)
          .mul(slippageMultiplier)
          .div(collateralPrice)
          .mul(collateralMintFactor)
          .floor();

        const minCollateral = new BN(expectedCollateral.toFixed(0));
        ```
      </Step>

      <Step>
        ### Build the Liquidation Transaction

        `buildLiquidateTxns` constructs the full transaction including reserve refreshes, obligation refresh, ATA creation, compute budget, and the liquidation instruction.

        ```typescript theme={null}
        const action = await KaminoAction.buildLiquidateTxns({
          kaminoMarket: market!,
          amount: repayAmount,
          minCollateralReceiveAmount: minCollateral,
          repayReserveAddress: targetDebt!.reserveAddress,
          withdrawReserveAddress: targetCollateral!.reserveAddress,
          liquidator,
          obligationOwner: obligation.state.owner,
          obligation,
          useV2Ixs: true,
          extraComputeBudget: 1_400_000,
          includeAtaIxs: true,
          requestElevationGroup: false,
          initUserMetadata: { skipInitialization: true, skipLutCreation: true },
          maxAllowedLtvOverridePercent: 0,
          currentSlot,
        });
        ```
      </Step>

      <Step>
        ### Assemble Instructions

        ```typescript theme={null}
        const instructions = [
          ...action.computeBudgetIxs,
          ...action.setupIxs,
          ...action.lendingIxs,
          ...action.cleanupIxs,
        ];
        ```
      </Step>

      <Step>
        ### Sign and Send

        ```typescript theme={null}
        const { value: blockhash } = await rpc
          .getLatestBlockhash({ commitment: 'finalized' })
          .send();

        const signed = await pipe(
          createTransactionMessage({ version: 0 }),
          (tx) => setTransactionMessageFeePayerSigner(liquidator, tx),
          (tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),
          (tx) => appendTransactionMessageInstructions(instructions, tx),
          (tx) => signTransactionMessageWithSigners(tx),
        );

        await sendAndConfirm(signed, { commitment: 'confirmed', skipPreflight: true });

        console.log('Liquidation executed:', getSignatureFromTransaction(signed));
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Rust">
    <Steps>
      <Step>
        ### Fetch Obligation and Reserves

        Use `ObligationContext` to discover all reserves referenced by the obligation, then fetch them in one RPC call.

        ```rust theme={null}
        use klend_interface::{
            helpers::refresh,
            instructions::liquidate::{
                liquidate_obligation_and_redeem_reserve_collateral_v2,
                LiquidateObligationAndRedeemReserveCollateralV2Accounts,
            },
            pda,
            state::{LendingMarket, Obligation, Reserve, from_account_data},
            Fraction, ObligationContext, ObligationInfo, ReserveInfo, KLEND_PROGRAM_ID,
        };
        use solana_instruction::AccountMeta;
        use solana_sdk::signer::{keypair::read_keypair_file, Signer};

        let signer = read_keypair_file("/path/to/liquidator-keypair.json")
            .expect("Failed to read keypair file");
        let owner = signer.pubkey();

        let obligation_account = rpc_client.get_account(&obligation_pubkey)?;
        let obligation = from_account_data::<Obligation>(&obligation_account.data)?;

        let reserve_addrs = ObligationContext::reserve_addresses_for_obligation(
            &obligation_account.data,
        )?;
        let reserve_accounts = rpc_client.get_multiple_accounts(&reserve_addrs)?;

        let parsed_reserves: Vec<(Pubkey, Reserve)> = reserve_addrs
            .iter()
            .zip(reserve_accounts.iter())
            .filter_map(|(pk, acc)| {
                acc.as_ref().and_then(|a| {
                    from_account_data::<Reserve>(&a.data).ok().map(|r| (*pk, *r))
                })
            })
            .collect();

        let repay_reserve = parsed_reserves.iter()
            .find(|(pk, _)| *pk == repay_reserve_pubkey)
            .map(|(_, r)| r)
            .expect("Repay reserve not found");

        let withdraw_reserve = parsed_reserves.iter()
            .find(|(pk, _)| *pk == withdraw_reserve_pubkey)
            .map(|(_, r)| r)
            .expect("Withdraw reserve not found");
        ```
      </Step>

      <Step>
        ### Calculate Repay Amount

        Fetch the `LendingMarket` account to read the close factor, then calculate the maximum repayable amount from the borrow position.

        ```rust theme={null}
        let borrow_position = obligation.borrows.iter()
            .find(|b| b.borrow_reserve == repay_reserve_pubkey)
            .expect("Borrow position not found");

        let borrowed_amount: u64 = Fraction::from_bits(
            borrow_position.borrowed_amount()
        ).to_num();

        let market_account = rpc_client.get_account(&obligation.lending_market)?;
        let lending_market = from_account_data::<LendingMarket>(&market_account.data)?;

        let close_factor = lending_market.liquidation_max_debt_close_factor_pct;
        let repay_amount = (borrowed_amount as f64 * close_factor as f64 / 100.0) as u64;
        ```
      </Step>

      <Step>
        ### Swap into Debt Token via KSwap

        If the liquidator does not hold the debt token, swap into it using the KSwap REST API before liquidating.

        ```rust theme={null}
        use serde::Deserialize;
        use solana_sdk::transaction::VersionedTransaction;

        #[derive(Deserialize)]
        struct KswapResponse { data: KswapData }

        #[derive(Deserialize)]
        #[serde(rename_all = "camelCase")]
        struct KswapData {
            transaction: String,
            expected_amount_out: String,
            min_amount_out: String,
        }

        let liquidator_debt_ata =
            spl_associated_token_account::get_associated_token_address_with_program_id(
                &owner,
                &repay_reserve.liquidity.mint_pubkey,
                &repay_reserve.liquidity.token_program,
            );

        use spl_token::state::Account as TokenAccount;
        use solana_sdk::program_pack::Pack;

        let wallet_balance = rpc_client.get_account(&liquidator_debt_ata)
            .ok()
            .and_then(|a| TokenAccount::unpack(&a.data).ok())
            .map(|acct| acct.amount)
            .unwrap_or(0);

        if wallet_balance < repay_amount {
            let wsol = Pubkey::from_str("So11111111111111111111111111111111111111112")?;

            // Calculate how much SOL to swap based on debt USD value
            let debt_price: f64 = Fraction::from_bits(
                u128::from(repay_reserve.liquidity.market_price_sf)
            ).to_num();
            let repay_usd = repay_amount as f64
                / 10f64.powi(repay_reserve.liquidity.mint_decimals as i32) * debt_price;
            let sol_price = parsed_reserves.iter()
                .find(|(_, r)| r.liquidity.mint_pubkey == wsol)
                .map(|(_, r)| Fraction::from_bits(
                    u128::from(r.liquidity.market_price_sf)
                ).to_num::<f64>())
                .unwrap_or(100.0);
            let sol_balance = rpc_client.get_balance(&owner)?;
            let swap_amount = (repay_usd / sol_price * 1e9) as u64;
            let swap_amount = swap_amount.min(sol_balance.saturating_sub(5_000_000));

            let client = reqwest::blocking::Client::new();
            let resp = client
                .get("https://api.kamino.finance/kswap/swap/")
                .query(&[
                    ("tokenIn", wsol.to_string()),
                    ("tokenOut", repay_reserve.liquidity.mint_pubkey.to_string()),
                    ("amountIn", swap_amount.to_string()),
                    ("maxSlippageBps", "50".to_string()),
                    ("wallet", owner.to_string()),
                    ("wrapAndUnwrapSol", "true".to_string()),
                ])
                .send()?;

            if !resp.status().is_success() {
                let status = resp.status();
                let body = resp.text().unwrap_or_default();
                return Err(format!("KSwap API error {status}: {body}").into());
            }

            let kswap: KswapResponse = resp.json()?;

            // Decode, re-sign, and send the swap transaction
            use base64::Engine;
            let tx_bytes = base64::engine::general_purpose::STANDARD
                .decode(&kswap.data.transaction)?;
            let swap_tx: VersionedTransaction = bincode::deserialize(&tx_bytes)?;
            let blockhash = rpc_client.get_latest_blockhash()?;
            let mut msg = swap_tx.message;
            msg.set_recent_blockhash(blockhash);
            let signed = VersionedTransaction::try_new(msg, &[&signer])?;
            rpc_client.send_and_confirm_transaction_with_spinner(&signed)?;
        }
        ```
      </Step>

      <Step>
        ### Derive Token Accounts

        Derive ATAs using the actual token programs from the reserve data. The collateral cToken mint address is read directly from the `Reserve` struct.

        ```rust theme={null}
        let user_source_liquidity =
            spl_associated_token_account::get_associated_token_address_with_program_id(
                &owner,
                &repay_reserve.liquidity.mint_pubkey,
                &repay_reserve.liquidity.token_program,
            );

        let ctoken_mint = withdraw_reserve.collateral.mint_pubkey;
        let ctoken_program = rpc_client.get_account(&ctoken_mint)
            .map(|a| a.owner)
            .unwrap_or(spl_token::ID);

        let user_destination_collateral =
            spl_associated_token_account::get_associated_token_address_with_program_id(
                &owner, &ctoken_mint, &ctoken_program,
            );

        let user_destination_liquidity =
            spl_associated_token_account::get_associated_token_address_with_program_id(
                &owner,
                &withdraw_reserve.liquidity.mint_pubkey,
                &withdraw_reserve.liquidity.token_program,
            );
        ```
      </Step>

      <Step>
        ### Create Token Accounts

        Create the associated token accounts if they do not already exist. Uses `create_associated_token_account_idempotent` which is a no-op if the account is already initialized.

        ```rust theme={null}
        let ata_ixs = vec![
            spl_associated_token_account::instruction::create_associated_token_account_idempotent(
                &owner, &owner, &repay_reserve.liquidity.mint_pubkey, &repay_reserve.liquidity.token_program,
            ),
            spl_associated_token_account::instruction::create_associated_token_account_idempotent(
                &owner, &owner, &ctoken_mint, &ctoken_program,
            ),
            spl_associated_token_account::instruction::create_associated_token_account_idempotent(
                &owner, &owner, &withdraw_reserve.liquidity.mint_pubkey, &withdraw_reserve.liquidity.token_program,
            ),
        ];

        let blockhash = rpc_client.get_latest_blockhash()?;
        let ata_tx = Transaction::new(&[&signer], Message::new(&ata_ixs, Some(&owner)), blockhash);
        rpc_client.send_and_confirm_transaction(&ata_tx)?;
        ```
      </Step>

      <Step>
        ### Build Liquidation Instructions

        Build refresh instructions for each reserve and the obligation, then the liquidation instruction using actual addresses from the `Reserve` struct.

        ```rust theme={null}
        use solana_sdk::transaction::Transaction;
        use solana_sdk::message::Message;

        let (lma, _) = pda::lending_market_authority(
            &KLEND_PROGRAM_ID, &repay_reserve.lending_market,
        );

        let obligation_info = ObligationInfo::from_obligation(
            obligation_pubkey, obligation,
        );

        let all_reserve_infos: Vec<ReserveInfo> = parsed_reserves.iter()
            .map(|(pk, r)| ReserveInfo::from_reserve(*pk, r))
            .collect();

        // Refresh reserves and obligation
        let mut instructions = Vec::new();
        for info in &all_reserve_infos {
            if obligation_info.deposit_reserves.contains(&info.address)
                || obligation_info.borrow_reserves.contains(&info.address)
            {
                instructions.push(refresh::refresh_reserve(info));
            }
        }
        instructions.push(refresh::refresh_obligation(
            &repay_reserve.lending_market, &obligation_info,
        ));

        // Remaining accounts: deposit reserves + borrow reserves
        let mut remaining: Vec<AccountMeta> = Vec::new();
        for r in &obligation_info.deposit_reserves {
            remaining.push(AccountMeta::new(*r, false));
        }
        for r in &obligation_info.borrow_reserves {
            remaining.push(AccountMeta::new(*r, false));
        }

        // Calculate slippage protection to guard against sandwich attacks.
        // Without this, a malicious validator could reorder transactions
        // to extract the full liquidation bonus.
        const MIN_RECEIVED_SLIPPAGE_BPS: u16 = 200; // 2%

        let collateral_price: f64 = Fraction::from_bits(
            u128::from(withdraw_reserve.liquidity.market_price_sf)
        ).to_num();
        let collateral_decimals = withdraw_reserve.liquidity.mint_decimals;
        let bonus_bps = withdraw_reserve.config.min_liquidation_bonus_bps as f64;
        let protocol_fee_pct = withdraw_reserve.config.protocol_liquidation_fee_pct as f64;

        let debt_price: f64 = Fraction::from_bits(
            u128::from(repay_reserve.liquidity.market_price_sf)
        ).to_num();
        let repay_usd = repay_amount as f64
            / 10f64.powi(repay_reserve.liquidity.mint_decimals as i32) * debt_price;

        let min_received = if collateral_price > 0.0 {
            let collateral_value = repay_usd * (1.0 + bonus_bps / 10_000.0);
            let after_fee = collateral_value * (1.0 - protocol_fee_pct / 100.0);
            let expected_tokens = after_fee / collateral_price
                * 10f64.powi(collateral_decimals as i32);
            let with_slippage = expected_tokens
                * (10_000.0 - MIN_RECEIVED_SLIPPAGE_BPS as f64) / 10_000.0;
            (with_slippage as u64).max(1)
        } else {
            1
        };

        // Liquidation instruction with actual reserve struct addresses
        instructions.push(liquidate_obligation_and_redeem_reserve_collateral_v2(
            LiquidateObligationAndRedeemReserveCollateralV2Accounts {
                liquidator: owner,
                obligation: obligation_pubkey,
                lending_market: repay_reserve.lending_market,
                lending_market_authority: lma,
                repay_reserve: repay_reserve_pubkey,
                repay_reserve_liquidity_mint: repay_reserve.liquidity.mint_pubkey,
                repay_reserve_liquidity_supply: repay_reserve.liquidity.supply_vault,
                withdraw_reserve: withdraw_reserve_pubkey,
                withdraw_reserve_liquidity_mint: withdraw_reserve.liquidity.mint_pubkey,
                withdraw_reserve_collateral_mint: withdraw_reserve.collateral.mint_pubkey,
                withdraw_reserve_collateral_supply: withdraw_reserve.collateral.supply_vault,
                withdraw_reserve_liquidity_supply: withdraw_reserve.liquidity.supply_vault,
                withdraw_reserve_liquidity_fee_receiver: withdraw_reserve.liquidity.fee_vault,
                user_source_liquidity,
                user_destination_collateral,
                user_destination_liquidity,
                repay_liquidity_token_program: repay_reserve.liquidity.token_program,
                withdraw_liquidity_token_program: withdraw_reserve.liquidity.token_program,
                collateral_obligation_farm_user_state: None,
                collateral_reserve_farm_state: None,
                debt_obligation_farm_user_state: None,
                debt_reserve_farm_state: None,
            },
            repay_amount,
            min_received,
            0, // max_allowed_ltv_override_percent (must be 0 on mainnet)
            remaining,
        ));
        ```

        <Warning>
          The `helpers::liquidate::liquidate()` helper uses `ReservePdas::derive()` which does not match reserves created by older program versions. Use the actual addresses from the `Reserve` struct as shown above.
        </Warning>
      </Step>

      <Step>
        ### Simulate and Send

        ```rust theme={null}
        let recent_blockhash = rpc_client.get_latest_blockhash()?;
        let message = Message::new(&instructions, Some(&owner));
        let tx = Transaction::new(&[&signer], message, recent_blockhash);

        // Simulate first
        let sim = rpc_client.simulate_transaction(&tx)?;
        if let Some(err) = sim.value.err {
            println!("Simulation failed: {err}");
            return Ok(());
        }

        // Send with fresh blockhash
        let fresh_blockhash = rpc_client.get_latest_blockhash()?;
        let fresh_msg = Message::new(&instructions, Some(&owner));
        let fresh_tx = Transaction::new(&[&signer], fresh_msg, fresh_blockhash);
        let signature = rpc_client.send_and_confirm_transaction(&fresh_tx)?;
        println!("Liquidation executed: {signature}");
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>
