Appearance
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:
TBDStrategy:
TBDNetwork: SEI mainnet (chain ID 1329)
Responsibilities
The keeper manages both vaults with the same logic running independently per vault.
| Task | Frequency | Description |
|---|---|---|
| Harvest | Every 4 hours | Claim rewards, compound yield (per vault) |
| Rebalance | On threshold | Shift allocations to higher-yield protocols (per vault) |
| Monitor | Continuous | Track APYs, balances, health (all protocols) |
| Alert | On events | Telegram 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:
- Check pending rewards from each yield source
- Skip if too small — if total pending < minimum threshold (e.g., $10), skip to save gas
- Query yield source count from strategy to determine array length
- Calculate fair prices for reward token → underlying asset swaps using DEX quotes for each enabled source
- Apply slippage tolerance (typically 1%) to determine
minAmountOutfor each source:typescriptFor yield sources like Feather (WSEI vault) where yield accrues as share value — no reward swap occurs —minAmountsOut[i] = dexQuote * 0.99 // 1% slippage toleranceminAmountsOut[i] = 0. - Call harvest:typescript
await vault.harvest(minAmountsOut); - 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
- 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
minAmountOutcalculated 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 saveMonitoring
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.