# Keeper Bot

* Off-chain process that calls the strategy + lending cranks on a schedule
* **Any wallet can step in** unless `vault.authorized_keeper` is set
* The keeper is just convenience, not a trust assumption

> Reference implementation: [`scripts/mainnet/keeper-basis.ts`](https://github.com/kamwithak/keystone-contracts/blob/main/scripts/mainnet/keeper-basis.ts). TypeScript + Anchor + Drift SDK + Jupiter API.

***

## Cadence

| Instruction                        | Frequency                                                                                                | Trigger                                                                                                                     |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `settle`                           | Every \~1 hour                                                                                           | Refresh funding EMA, drawdown peak, LST depeg check, `settle_pnl` if open                                                   |
| `attest_nav(new_nav_usdc)`         | After `settle`                                                                                           | Snap NAV to live position value when on-chain reads can't capture unrealized PnL. Bounded by `max_nav_change_bps_per_hour`. |
| `open_position` / `open_reverse`   | When the smoothed funding signal clears the threshold and dwell has elapsed                              | One-shot per mode entry                                                                                                     |
| `close_position` / `close_reverse` | When the signal falls back through the threshold                                                         | One-shot per mode exit                                                                                                      |
| `lend_idle_usdc` / `unlend_usdc`   | Whenever idle vault USDC exceeds the buffer target by > a margin (or buffer is short)                    | Keep USDC productive without breaching `liquidity_buffer_target`                                                            |
| `lend_reserve` / `unlend_reserve`  | Same, for the reserve fund                                                                               | Reserve fund stays in Kamino unless an admin draw is queued                                                                 |
| `process_withdrawal`               | When the queue has unprocessed requests AND vault holds enough idle USDC for the **next** request (FIFO) | Permissionless — anyone can crank                                                                                           |
| `emergency_close`                  | When drawdown guard trips OR vault is paused with a position open                                        | Permissionless once tripped                                                                                                 |

***

## Signal pipeline (open / close decisions)

1. **Read funding.** `settle` parses `last_funding_rate` and `last_funding_rate_ts` from Drift's `PerpMarket(SOL-PERP)` account and updates the on-chain EMA (`funding_apr_smoothed_bps`). Sanity-bounded — absurd values revert with `FundingRateInsane`.
2. **Stale check.** If `now - funding_smooth_last_ts > funding_max_staleness_seconds`, opens revert with `FundingSignalStale`. Run `settle` first.
3. **Threshold + dwell + drawdown latency.**
   * If `funding_apr_smoothed_bps ≥ funding_threshold_normal_bps` AND `now − last_mode_change_ts ≥ min_dwell_seconds` → call `open_position` (or `close_reverse` first if currently in reverse).
   * If `funding_apr_smoothed_bps ≤ funding_threshold_reverse_bps` AND dwell ok → call `open_reverse` (or `close_position` first if currently in normal).
   * Else if in active mode but signal decayed below threshold + hysteresis → call the corresponding close.
   * Drawdown trips require `consecutive_dd_settles_observed ≥ consecutive_dd_settles_required` (default 2) before `emergency_close` can fire on a NAV breach — single-tick noise is rejected with `DrawdownTriggerLatent`.
4. **Build the transaction.** Assemble per-instruction `remaining_accounts` groups (see [Instructions](/for-developers/instructions.md)) and the Jupiter swap data via the Jupiter `/swap-instructions` endpoint.
5. **Submit.** Wrap with priority-fee compute budget instructions. Retry on slot-out / blockhash-not-found.

***

## Assembling `remaining_accounts`

* Strategy instructions pack multiple CPIs into one transaction
* Each group needs a precise account list — wrong order or missing accounts reverts the CPI
* Use the venue's official client / API to build each group:
* **Drift deposit / withdraw / place\_perp\_order / settle\_pnl** — use `@drift-labs/sdk` (`DriftClient.getDepositInstruction`, etc.) and extract the `keys` array from the built instructions.
* **Kamino refresh\_obligation / deposit\_obligation\_collateral / borrow\_obligation\_liquidity / repay / withdraw** — use the Kamino IDL and PDA derivation utilities; obligation reserves auto-derive once you know the market + reserve indexes.
* **Jupiter route\_swap** — call Jupiter's `/swap-instructions` HTTP endpoint with `userPublicKey = vaultPda`, then pass the returned `swapInstruction.data` as `jupiter_swap_data` and the returned `keys` as the relevant remaining-accounts group.

> Pass per-group counts as the u8 instruction args (`jupiter_account_count`, `drift_deposit_account_count`, etc.) so the on-chain handler can split safely.

***

## Process-withdrawal cranking

**Strict FIFO:**

* Next-in-line request\_id is `vault.queue_processed_through + 1`
* Any other request errors with `WithdrawalNotNextInQueue`
* Anyone can crank (including the original requester)
* Rent refunds to the original requester even if a different wallet pays the tx

**Liquidity shortfall:**

* Reverts with `InsufficientLiquidityBuffer` if vault USDC is short of the next `usdc_owed`
* Keeper should `unlend_usdc` first if Kamino USDC can cover the shortfall
* Otherwise wait for the next `close_*` to free up USDC

***

## Failure modes

| Symptom                                              | Cause                                                                  | Resolution                                                                                                       |
| ---------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `MinDwellNotElapsed`                                 | Tried to switch modes too soon                                         | Wait for `last_mode_change_ts + min_dwell_seconds`                                                               |
| `FundingThresholdNotMet`                             | Smoothed funding hasn't crossed the threshold                          | Re-read `funding_apr_smoothed_bps`                                                                               |
| `FundingSignalStale`                                 | EMA older than `funding_max_staleness_seconds`                         | Run `settle` first                                                                                               |
| `SlippageExceeded`                                   | Jupiter route filled at worse price than expected                      | Refresh the quote, narrow the bound, retry                                                                       |
| `DriftCpiFailed`                                     | Drift returned an error — usually margin or oracle                     | Inspect program logs; may need `settle_pnl` first                                                                |
| `KaminoLtvExceeded`                                  | Borrow size too large for current collateral                           | Reduce `jitosol_borrow_amount` or post more USDC                                                                 |
| `InsufficientLiquidityBuffer` (`process_withdrawal`) | Not enough idle USDC for the next request                              | `unlend_usdc` to refill, or wait for next `close_*`                                                              |
| `LendingBreachesBuffer`                              | `lend_idle_usdc` would drop the buffer below `liquidity_buffer_target` | Reduce amount or run `unlend_usdc` instead                                                                       |
| `NavChangeExceedsCap`                                | `attest_nav` delta > `max_nav_change_bps_per_hour`                     | Stage with smaller deltas over multiple hours, or admin raises the cap if the position genuinely moved that much |
| `DrawdownTriggerLatent`                              | Drawdown observed but not yet across consecutive settles               | Wait one more settle cycle; this is by design                                                                    |
| `LstDepeg`                                           | jitoSOL/SOL deviation > `lst_depeg_bps`                                | `settle` auto-pauses; admin investigates and either unpauses or emergency-closes                                 |

***

## Idempotency

* All open / close instructions assert `position_mode` invariants on entry
* Duplicate calls during a retry storm revert cleanly without state damage
* `settle` is fully idempotent within an hour (no state change beyond the EMA refresh)

***

## Related

* [Admin operations](/for-operators/admin-ops.md)
* [Monitoring](/for-operators/monitoring.md)
* [Instructions reference](/for-developers/instructions.md) · [Errors](/for-developers/errors.md)


---

# Agent Instructions: 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.keystonefi.xyz/for-operators/keeper-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.
