Keeper Network
Keepers automate subscription charges and earn a 0.5% bounty per charge.
What is a keeper?
A keeper is any wallet running software that monitors on-chain subscriptions and callscharge() when they become due. Keepers are permissionless — anyone can run one. They earn a 0.5% bounty on every successful charge.
The reference keeper (@cron/keeper) is production-grade and handles discovery, event watching, gas checks, and batch execution.
Quick start
cd packages/keeper
cp .env.example .envEdit .env:
# Required
KEEPER_PRIVATE_KEY=0x... # Wallet that executes charges and receives bounties
CHAIN=baseSepolia # base | baseSepolia | arbitrum | arbitrumSepolia
CRON_REGISTRY_ADDRESS=0x... # Deployed CronRegistry address
RPC_URL=https://sepolia.base.org # RPC endpoint
# Optional tuning
BATCH_SIZE=10 # Max subscriptions per batch charge tx
POLL_INTERVAL_MS=60000 # How often to poll for due charges (default: 1 min)
MAX_GAS_PRICE_GWEI=50 # Skip charges if gas exceeds this
MIN_PROFIT_USD=0.01 # Skip charges below this profitability threshold
DISCOVERY_FROM_BLOCK=0 # Start block for bootstrapping subscription historynpm run startHow the keeper works
- Bootstrap — on startup, reads all
SubscriptionCreatedevents fromDISCOVERY_FROM_BLOCKto build an initial subscription list. - Live watching — subscribes to real-time events to catch new subscriptions and cancellations.
- Poll loop — every
POLL_INTERVAL_MS, filters due subscriptions and executes batch charges. - Gas guard — checks current gas price before executing. Skips if gas exceeds
MAX_GAS_PRICE_GWEI. - Profitability filter — calculates expected bounty minus estimated gas cost. Skips unprofitable charges.
Keeper statistics
The keeper logs statistics to stdout on each cycle:
[keeper] Cycle #42
Subscriptions tracked: 1,247
Due this cycle: 18
Charged: 17 | Failed: 1
Bounty earned: 0.085 USDC
Total bounty all-time: 12.43 USDC
Avg gas price: 0.08 gweiRunning multiple keepers
The protocol is fully permissionless — multiple keepers can operate simultaneously. If two keepers try to charge the same subscription, the second transaction reverts (the subscription is no longer due). Only the first keeper earns the bounty.
[*] Running in production
pm2 or a Docker container to keep the keeper running continuously. Set DISCOVERY_FROM_BLOCK to the block your registry was deployed at to speed up the initial sync.# Run with pm2
npm install -g pm2
pm2 start npm --name "cron-keeper" -- run start
pm2 save
pm2 startupBuild your own keeper
You can build a custom keeper using the SDK directly:
import { CronClient } from "@cron/sdk";
const cron = new CronClient({
chain: "base",
walletClient, // keeper wallet
});
async function chargeLoop() {
// Get all subscriptions that are currently due
const due = await cron.charges.getDue();
for (const { planId, subscriber } of due) {
try {
const tx = await cron.charges.charge({ planId, subscriber });
console.log("Charged:", tx.hash);
} catch (err) {
// Subscription may have already been charged by another keeper
console.error("Charge failed:", err.message);
}
}
}
setInterval(chargeLoop, 60_000);