Running the buyback keeper
The keeper is a script, not a privileged oracle. It computes a defensible minTokensOut, verifies the route, simulates, and only then submits.
Crucially, the on-chain bounds do not depend on it behaving well — see keeper constraints.
Running it
pnpm keeper:buyback # plan and simulate onlypnpm keeper:buyback -- --eth 0.05 # size the spend explicitlypnpm keeper:buyback -- --slippage 200 # 2.00% tolerance (default 1.00%)pnpm keeper:buyback -- --broadcast # submitBroadcasting requires PLOUTO_KEEPER_KEY. Without both the flag and the key, it will not send a transaction.
What it checks first
- Chain ID is 4663.
- The registry is initialized.
- The executor is not paused.
- The budget clears
minEthPerExecution. - The factory record's curve matches the registry's curve.
- The phase is tradeable — 0 or 2, never 1 or 3.
Sizing
Respects every on-chain cap, and drains a small residual rather than stranding it:
const shareCap = (budget * shareBps) / 10_000n;let ethIn = opt('eth') ? parseEther(opt('eth')!) : budget <= maxPerExec ? budget : shareCap;if (ethIn > maxPerExec) ethIn = maxPerExec;if (ethIn > budget) ethIn = budget;if (ethIn > shareCap && ethIn !== budget) ethIn = shareCap;The contract allows spending the entire budget when it is at or below the per-execution cap, so a residual can always be cleared.
Quoting, pre-graduation
Constant product against the phantom-inclusive reserves, net of the curve fee:
const netIn = (ethIn * (10_000n - feeBps)) / 10_000n;expectedOut = (tokenReserve * netIn) / (quoteReserve + netIn);It also refuses to trade while the snipe tax is active:
if (snipeBps > 0n) fail(`the snipe tax is still active (${snipeBps} bps). Wait for it to decay.`);Quoting, post-graduation
From the live pool price, read the same way the contract reads it:
const sqrtPriceX96 = await client.readContract({ address: executor, abi: buybackExecutorAbi, functionName: 'poolSqrtPriceX96', args: [token],});const Q96 = 1n << 96n;expectedOut = (((ethIn * sqrtPriceX96) / Q96) * sqrtPriceX96) / Q96;This is a spot quote and ignores depth, which is precisely why the on-chain price-impact bound is the real guard.
The floor
const minTokensOut = (expectedOut * (10_000n - SLIPPAGE_BPS)) / 10_000n;if (minTokensOut === 0n) fail('computed minTokensOut is zero — refusing to submit an unprotected buy');A zero floor is rejected locally, and the contract rejects it too with ZeroMinTokensOut().
Simulate, then submit
const sim = await client.simulateContract({ address: executor, abi: buybackExecutorAbi, functionName: 'executeBuyback', args: [ethIn, minTokensOut, deadline], account: keeper,});// ... only then:const hash = await wallet.writeContract(sim.request);The script prints the simulated retirement and the realised slippage versus spot before it broadcasts.
The revenue keeper
pnpm keeper:route # report the four fee states, simulatepnpm keeper:route -- --broadcastClaiming and routing are permissionless, so this exists to run on a schedule rather than to hold a permission. It prints the escrow balance, unallocated revenue, held donations and lifetime totals every run.
Operational advice
- Give the keeper a dedicated low-value hot wallet holding only gas.
- Keep
maxPriceImpactBpstight. The default is 500 (5%); widening it widens the sandwich window. - Run buybacks at irregular intervals and sizes rather than on a predictable schedule.
- Monitor the executor's
paused()state — a paused executor means someone intervened.