# keeper

The keeper is the off-chain operator that bridges basefun smart contracts to Avantis and to the fee distribution machinery. It runs as a Node.js worker on Railway, with its own Postgres bookkeeping tables.

## Responsibilities

| Job                           | Trigger                                 | What it does                                               |
| ----------------------------- | --------------------------------------- | ---------------------------------------------------------- |
| Open Avantis perp on LT mint  | `Minted` event from LT contracts        | `Avantis.delegatedAction(keeper, openTradeCalldata)`       |
| Bookkeeping after open        | After Avantis tx confirms               | `LT.notePositionOpened()`                                  |
| Ensure LT allowance           | First-time setup per LT                 | `USDC.approve(LT, MAX)` so sells can `transferFrom` keeper |
| Auto-migrate graduated curves | `Graduated` event on any curve          | `FactoryV3.migrateToUniV2(curve)`                          |
| Sweep post-grad fees          | Every tick, for each graduated V2 token | Convert token FOT fees to USDC, dispatch 50/50             |

## Wallet

The keeper's wallet is an EOA shared across all the above jobs. It holds:

* A small ETH balance for gas (\~0.001 ETH ≈ enough for 50 openTrades on Base).
* USDC moving in (from LT mints + LT redeems) and out (Avantis opens, treasury/creator dispatches).
* Token balances of post-grad V2 tokens, accumulated via the 1% FOT skim, between sweeps.

When the ETH balance drops below \~0.0008 ETH, the team tops it up manually. The codebase doesn't push for ETH automatically.

## Avantis open flow (per LT mint)

```
LT.Minted(minter, usdcIn, ltOut, newSupply)
  → keeper reads usdcIn
  → if usdcIn < $101 (Avantis notional minimum for 5×):
        accumulate into a pending bucket per LT
        sweep when bucket >= $101
    else:
        immediately:
          calldata = LT.encodeOpenTrade(usdcIn)
          tx = Avantis.delegatedAction(keeper, calldata)
          await receipt
          LT.notePositionOpened()
```

Failure cases (Avantis rejection, RPC outage) are logged. The keeper does not retry indefinitely on the same notional; it falls back to the pending bucket and tries again on the next LT mint.

## Auto-migration

Every tick the keeper queries the DB for tokens with `graduated = true AND uniV2Pair IS NULL`. For each:

{% stepper %}
{% step %}

## Check graduation on-chain

Check on-chain that `curve.graduated() == true`
{% endstep %}

{% step %}

## Migrate the curve

Call `factory.migrateToUniV2(curve)`
{% endstep %}

{% step %}

## Let the indexer write the pair

On success, the indexer will pick up the Migrated event and write `uniV2Pair`
{% endstep %}
{% endstepper %}

Whitelisted as `migrators[keeper] = true` on both FactoryV2 and FactoryV3, set by the factory owner post-deploy.

## Fee sweep (post-grad)

Detailed in [Fees post-bond](broken://pages/4a57e3da8c1f1277bc221ed972c454a8791b7fc5). High-level loop, per V2 graduated token, per tick:

{% stepper %}
{% step %}

## Read the keeper balance

`keeperBal = token.balanceOf(keeper)`

If `keeperBal == 0`, skip.
{% endstep %}

{% step %}

## Resolve the pair

Resolve `uniV2Pair` (cached).
{% endstep %}

{% step %}

## Resolve recipients

Resolve `(treasury, creator)` by reading the token's immutable storage (cached).
{% endstep %}

{% step %}

## Estimate the USDC value

`estUSDC = router.getAmountsOut(keeperBal, [token, USDC])`

If `estUSDC < $5`, skip (dust gate).
{% endstep %}

{% step %}

## Approve the router once

Ensure `token.approve(router, MAX)` once.
{% endstep %}

{% step %}

## Record the pre-swap USDC balance

`usdcBefore = USDC.balanceOf(keeper)`
{% endstep %}

{% step %}

## Swap the tokens

`router.swapExactTokensForTokensSupportingFeeOnTransferTokens( keeperBal, minOut (3% slippage), [token, USDC], keeper, +5min )`
{% endstep %}

{% step %}

## Compute the amount received

`received = USDC.balanceOf(keeper) - usdcBefore`
{% endstep %}

{% step %}

## Dispatch the proceeds

`USDC.transfer(treasury, received / 2)`\
`USDC.transfer(creator, received - received / 2)`
{% endstep %}
{% endstepper %}

`(treasury, creator)` come **from the token's bytecode** (`token.treasury()`, `token.creatorFeeRecipient()`), not from the DB. There's no risk of paying the wrong wallet — the keeper reads each token's immutables independently per sweep.

## Per-token caches

To avoid spamming RPC reads, the keeper keeps in-memory caches:

| Cache            | Value                                | Cleared         |
| ---------------- | ------------------------------------ | --------------- |
| `pairCache`      | `tokenAddress → pairAddress`         | Process restart |
| `recipientCache` | `tokenAddress → (treasury, creator)` | Process restart |

Both are immutable per-token, so cache misses only happen on first sweep of a newly graduated token.

## Postgres tables (keeper-owned)

| Table               | Purpose                                                |
| ------------------- | ------------------------------------------------------ |
| `KeeperCursor`      | Last block scanned by the LT event listener            |
| `KeeperLT`          | Per-LT runtime state (pending bucket, last open block) |
| `KeeperPendingMint` | Mint events held up by the < $101 aggregation rule     |

Schema is created via raw SQL (`CREATE TABLE IF NOT EXISTS …`) at boot; the keeper does **not** use Prisma migrations (so it can't accidentally drop API-owned tables).

## Failure modes and how the keeper handles them

| Failure                                                               | What the keeper does                                                                                                                          |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Avantis `openTrade` reverts (price slippage > 3%)                     | Log + skip; next mint retries; eventually accumulates enough notional to retry                                                                |
| Pre-sell `transferFrom(keeper, …)` reverts because allowance is 0     | Manual operator runs `USDC.approve(LT, MAX)` from the keeper EOA. The keeper now does this on LT discovery automatically.                     |
| `migrateToUniV2` reverts because pair was front-run with hostile dust | Whitelisted-migrators check on the factory blocks this attack; if it still happens, manual reset of pair via Uni V2 admin functions is needed |
| Fee sweep swap reverts (low liquidity, slippage)                      | Log + skip this tick; next tick retries with the (possibly larger) accumulated balance                                                        |
| RPC outage                                                            | Log + sleep, retry on next tick                                                                                                               |
| ETH balance < min                                                     | Continues swapping calls until provider rejects with "insufficient funds for gas"; team tops up manually                                      |

The keeper never reverts the whole process on a single failure — every job is wrapped in try/catch with logging, so one bad token can't stall the whole loop.


---

# 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://basefun.gitbook.io/basefun-docs/keeper.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.
