Building a Keeper Bot
Keeper bots maintain auction health by creating checkpoints and triggering settlement. Checkpoints are snapshots of the auction state at specific auction blocks, required for bid settlement. Both checkpoint creation and the settlement pipeline are permissionless -- any wallet can submit these transactions.
Architecture
Poll Indexer -> Find auctions needing work -> Build tx -> Submit
A keeper bot handles two responsibilities:
- Checkpoint creation during active auctions
- Settlement pipeline after auctions end
Checkpoint Keeper
Step 1: Poll for Active Auctions
import { Connection, PublicKey, Keypair, Transaction } from "@solana/web3.js";
import {
findAuctionStatePda,
findCheckpointPda,
CCA_V2_PROGRAM_ID,
} from "@runner-protocol/sdk";
const INDEXER_URL = "https://cca-indexer-production.up.railway.app";
const connection = new Connection("https://api.devnet.solana.com");
async function getActiveAuctions() {
const response = await fetch(`${INDEXER_URL}/api/auctions?limit=100`);
const { data: auctions } = await response.json();
return auctions.filter(
(a: any) => Number(a.slots_remaining) > 0 && Number(a.start_slot) > 0
);
}
Step 2: Detect Checkpoint Needs
async function needsCheckpoint(auction: any): Promise<boolean> {
const currentSlot = await connection.getSlot("confirmed");
const startSlot = Number(auction.start_slot);
const blockInterval = Number(auction.auction_block_interval);
const latestCheckpointBlock = Number(
auction.latest_checkpoint_auction_block
);
if (currentSlot < startSlot) return false; // Not started yet
// Calculate current auction block
const currentAuctionBlock = Math.floor(
(currentSlot - startSlot) / blockInterval
);
// If current block > latest checkpoint, we need a new checkpoint
return currentAuctionBlock > latestCheckpointBlock;
}
Step 3: Build and Submit Checkpoint
async function createCheckpoint(auction: any, keeperKeypair: Keypair) {
const auctionConfig = new PublicKey(auction.pubkey);
const latestBlock = BigInt(auction.latest_checkpoint_auction_block);
const targetBlock = latestBlock + 1n;
const [auctionState] = findAuctionStatePda(auctionConfig);
const [checkpoint] = findCheckpointPda(auctionConfig, targetBlock);
const [latestCheckpoint] = findCheckpointPda(auctionConfig, latestBlock);
// Build the create_checkpoint instruction
// Discriminator: sha256("global:create_checkpoint")[..8]
// Args: auction_block (u64 LE)
// The instruction accounts are:
// payer (signer, writable), auction_config, auction_state (writable),
// supply_schedule, checkpoint (writable), system_program
// remaining_accounts: [latestCheckpoint, ...tick PDAs]
// After building the instruction, add to transaction and send
const tx = new Transaction().add(/* checkpoint instruction */);
const signature = await connection.sendTransaction(tx, [keeperKeypair]);
console.log(
`Checkpoint created for block ${targetBlock}: ${signature}`
);
}
Checkpoints may need multiple transactions if there are many active ticks. The program processes up to 10 ticks per call. If the checkpoint enters Resolving status, call create_checkpoint again with the same auction_block to resume.
Step 4: Main Loop
async function checkpointKeeperLoop(keeperKeypair: Keypair) {
while (true) {
try {
const auctions = await getActiveAuctions();
for (const auction of auctions) {
if (await needsCheckpoint(auction)) {
console.log(
`Creating checkpoint for ${auction.pubkey}`
);
await createCheckpoint(auction, keeperKeypair);
}
}
} catch (err) {
console.error("Keeper loop error:", err);
}
// Poll every 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
}
Settlement Keeper
After an auction ends, the settlement pipeline has four permissionless steps. A keeper bot can automate all of them.
Detect Ended Auctions Needing Settlement
async function getAuctionsNeedingSettlement() {
// Fetch launches with AuctionReady or later states
const response = await fetch(`${INDEXER_URL}/api/launches?limit=100`);
const { data: launches } = await response.json();
return launches.filter((launch: any) => {
const status = launch.display_status;
// These states have pending settlement work
return [
"AuctionEnded",
"Graduated",
"Finalized",
"ProceedsSwept",
"PoolCreated",
].includes(status);
});
}
Execute Settlement Steps
async function settleIfNeeded(launch: any, keeperKeypair: Keypair) {
const status = launch.display_status;
switch (status) {
case "AuctionEnded":
case "Graduated":
// Step 1: finalize_auction
await submitFinalize(launch, keeperKeypair);
break;
case "Finalized":
// Step 2: sweep_to_escrow
await submitSweep(launch, keeperKeypair);
break;
case "ProceedsSwept":
// Step 3: create_raydium_pool
await submitCreatePool(launch, keeperKeypair);
break;
case "PoolCreated":
// Step 4: enable_claims
await submitEnableClaims(launch, keeperKeypair);
break;
}
}
Settlement instructions are idempotent. If the state is already at or past the target state, the instruction returns Ok(()) without error. Keepers can safely retry without worrying about duplicate execution.
Combined Keeper Loop
async function keeperLoop(keeperKeypair: Keypair) {
while (true) {
try {
// Checkpoint duties
const activeAuctions = await getActiveAuctions();
for (const auction of activeAuctions) {
if (await needsCheckpoint(auction)) {
await createCheckpoint(auction, keeperKeypair);
}
}
// Settlement duties
const pendingSettlement = await getAuctionsNeedingSettlement();
for (const launch of pendingSettlement) {
await settleIfNeeded(launch, keeperKeypair);
}
} catch (err) {
console.error("Keeper error:", err);
}
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
}
Operational Considerations
Funding
The keeper wallet needs SOL for:
- Transaction fees (~5000 lamports per tx)
- Checkpoint account rent (~0.003 SOL per checkpoint account creation)
Monitoring
Use the indexer health endpoint to monitor lag:
const health = await fetch(`${INDEXER_URL}/api/health`).then((r) =>
r.json()
);
if (health.status !== "healthy") {
console.warn(`Indexer is ${health.status}, lag: ${health.lag_slots} slots`);
}
Concurrency
If multiple keepers are running, they may race to create the same checkpoint. This is safe -- the second transaction will fail with CheckpointAlreadyFinalized (6015), which keepers should catch and ignore.
Failed Auctions
For auctions that end without graduating, keepers can call recover_failed_auction on the Launchpad program to return tokens to the creator. Bidders can then call exit_bid on CCA V2 to receive full refunds.