Skip to content

Keeper Bot

The keeper bot is an off-chain TypeScript service that automates vault operations across both the USDC and WSEI vaults.

Deployed Addresses

USDC Vault

  • Vault: 0x1B85248a30C0A61DD52933C17c989d1B9aFDf98e
  • Strategy: 0xdD02a58a3515e78691260Ee3ed0ca87a540A8Dc2

WSEI Vault

  • Vault: TBD

  • Strategy: TBD

  • Network: SEI mainnet (chain ID 1329)

Responsibilities

The keeper manages both vaults with the same logic running independently per vault.

TaskFrequencyDescription
HarvestEvery 4 hoursClaim rewards, compound yield (per vault)
RebalanceOn thresholdShift allocations to higher-yield protocols (per vault)
MonitorContinuousTrack APYs, balances, health (all protocols)
AlertOn eventsTelegram notifications for significant events

Harvest Flow

The harvest logic is identical for both vaults. The keeper runs it independently for each, using the enhanced harvest(uint256[] calldata minAmountsOut) function for MEV protection:

  1. Check pending rewards from each yield source
  2. Skip if too small — if total pending < minimum threshold (e.g., $10), skip to save gas
  3. Query yield source count from strategy to determine array length
  4. Calculate fair prices for reward token → underlying asset swaps using DEX quotes for each enabled source
  5. Apply slippage tolerance (typically 1%) to determine minAmountOut for each source:
    typescript
    minAmountsOut[i] = dexQuote * 0.99  // 1% slippage tolerance
    For yield sources like Feather (WSEI vault) where yield accrues as share value — no reward swap occurs — minAmountsOut[i] = 0.
  6. Call harvest:
    typescript
    await vault.harvest(minAmountsOut);
  7. Strategy executes:
    • Claims all rewards from enabled yield sources
    • Swaps to underlying asset with slippage protection (reverts if price is manipulated)
    • Reports profit to vault (vault takes 10% fee)
    • Redeposits compounded asset
  8. Log yield amount and send Telegram summary

Slippage Calculation

The keeper calculates minimum output amounts off-chain to protect against MEV:

typescript
// Get number of yield sources
const yieldSourceCount = await strategy.getYieldSourceCount();

// Prepare minAmountsOut array
const minAmountsOut = new Array(yieldSourceCount);

// For each yield source
for (let i = 0; i < yieldSourceCount; i++) {
  const source = await strategy.yieldSources(i);

  if (!source.enabled) {
    minAmountsOut[i] = 0;
    continue;
  }

  // Get pending rewards for this source
  const pendingRewards = await getPendingRewards(source);

  if (pendingRewards.amount > 0) {
    // Get fair price from DEX
    const quote = await getSwapQuote(
      pendingRewards.token,
      underlyingAsset,
      pendingRewards.amount
    );

    // Apply 1% slippage tolerance
    minAmountsOut[i] = quote.outputAmount * 0.99;
  } else {
    minAmountsOut[i] = 0;
  }
}

// Call harvest with slippage protection
await vault.harvest(minAmountsOut);

Array length requirement: The minAmountsOut array must have exactly the same length as the number of yield sources in the strategy.

What happens on failure: If a sandwich bot or price manipulator tries to extract value, the actual swap output will be less than minAmountOut for at least one source, causing the entire transaction to revert.

Rebalance Logic

typescript
// Monitor APYs — runs per vault
const currentAPYs = await fetchAPYs(vaultConfig); // sources vary by vault

// Calculate optimal allocation
const optimalAllocation = calculateOptimal(currentAPYs);

// Get current allocation
const currentAllocation = await getYieldSourceSplits();

// Check deviation
const deviation = calculateDeviation(optimal, current);

// Rebalance if threshold exceeded and cooldown passed
if (deviation > rebalanceThreshold) {
  const lastRebalance = await strategy.lastRebalanceTime();
  const cooldown = await strategy.rebalanceCooldown();
  const now = Date.now() / 1000;

  if (now - lastRebalance > cooldown) {
    const lastSplitsChange = await strategy.lastSplitsTime();
    const splitsCooldown = await strategy.splitsCooldown();

    if (now - lastSplitsChange > splitsCooldown) {
      for (let i = 0; i < optimalAllocation.length; i++) {
        await strategy.updateYieldSourceSplit(i, optimalAllocation[i]);
      }
    }

    await strategy.rebalance();

    sendTelegramAlert({
      type: 'rebalance',
      vault: vaultConfig.name,
      old: currentAllocation,
      new: optimalAllocation,
      reason: `Deviation ${deviation.toFixed(2)}% exceeded threshold`
    });
  }
}

The APY sources vary by vault: the USDC vault monitors Yei, Takara, and Morpho; the WSEI vault monitors Yei, Takara, and Feather.

Cooldowns:

  • Rebalance cooldown: 1 hour (3600 seconds) between rebalances
  • Splits cooldown: 1 hour (3600 seconds) between split changes

MEV Protection

  • Slippage limits — all swaps require minAmountOut calculated off-chain from fair DEX quotes
  • Price validation — keeper fetches current prices before every harvest
  • Transaction reversion — if actual swap output < expected for any source, transaction reverts (funds safe)
  • Private transactions — submits through private mempools when available (optional)
  • Timing randomization — avoids predictable harvest patterns (optional)

Telegram Alerts

The bot sends alerts for:

  • ✅ Successful harvests (with yield amount, fees, and vault name)
  • 🔄 Rebalance events (old → new allocations, vault name, reason)
  • ⚠️ Errors or failed transactions
  • 📊 Daily summary reports (TVL, APY, yields per vault)
  • 🚨 Emergency events (large withdrawals, unusual APY changes, contract paused)
  • 💰 Performance stats (total yield harvested, fees collected)

Configuration

typescript
{
  // Network
  rpcUrl: 'https://evm-rpc.sei-apis.com',
  chainId: 1329,

  // Vaults (add entries for each vault to manage)
  vaults: [
    {
      name: 'USDC Vault',
      vaultAddress: '0x1B85248a30C0A61DD52933C17c989d1B9aFDf98e',
      strategyAddress: '0xdD02a58a3515e78691260Ee3ed0ca87a540A8Dc2',
      underlyingAsset: 'USDC',
    },
    {
      name: 'WSEI Vault',
      vaultAddress: 'TBD',
      strategyAddress: 'TBD',
      underlyingAsset: 'WSEI',
    },
  ],

  // Harvest settings
  harvestInterval: '4h',           // Harvest every 4 hours
  minHarvestAmount: '10',          // Min $10 yield to harvest (gas efficiency)
  slippageTolerance: 0.01,         // 1% slippage tolerance

  // Rebalance settings
  rebalanceThreshold: 100,         // 1% deviation (in basis points) triggers rebalance

  // Gas settings
  maxGasPrice: '1000',             // Max gas price in gwei (optional)
  gasMultiplier: 1.1,              // Gas estimate multiplier for safety

  // Alerts
  telegramChatId: '...',
  telegramBotToken: '...',

  // Keeper wallet
  privateKey: process.env.KEEPER_PRIVATE_KEY,
}

Tech Stack

  • Runtime: Node.js / TypeScript
  • Web3: ethers.js v6 or viem
  • Scheduling: Node cron or setInterval
  • Alerts: Telegram Bot API
  • Monitoring: Structured logging (Winston / Pino)
  • Error tracking: Sentry (optional)

Error Handling

typescript
try {
  await vault.harvest(minAmountsOut);
  logger.info('Harvest successful', { vault: vaultConfig.name, profit, fee });
  sendTelegramAlert('✅ Harvest successful', { vault: vaultConfig.name, profit, fee });
} catch (error) {
  logger.error('Harvest failed', { vault: vaultConfig.name, error });

  if (error.message.includes('SlippageExceedsCap')) {
    sendTelegramAlert('⚠️ Harvest failed: slippage too high', { vault: vaultConfig.name, error });
  } else if (error.message.includes('InvalidMinAmountsLength')) {
    sendTelegramAlert('🚨 Harvest failed: array length mismatch', { vault: vaultConfig.name, error });
    // Update local yield source count cache
  } else {
    sendTelegramAlert('❌ Harvest failed', { vault: vaultConfig.name, error });
  }

  // Don't throw — log and continue to next interval
}

Deployment

bash
# Install dependencies
npm install

# Configure environment
cp .env.example .env
# Edit .env with keeper private key, RPC URL, Telegram tokens

# Build
npm run build

# Run
npm start

# Or with PM2 for production
pm2 start dist/keeper.js --name kana-keeper
pm2 save

Monitoring

The keeper exposes a simple health check endpoint:

typescript
// Health check server
const server = express();
server.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    lastHarvest: lastHarvestTime,
    lastRebalance: lastRebalanceTime,
    uptime: process.uptime()
  });
});
server.listen(3000);

Monitor this endpoint with a service like UptimeRobot to get alerts if the keeper crashes.

Built on SEI