The keeper
The keeper is the automated process that runs the game. It opens rounds, claims fees, snapshots holders, rolls the draw, buys the stock and pays the winner. Everything else on this site is a view of what it did.
The loop
It is a single long-lived process, not a serverless function, because the round cycle has to keep running whether or not anyone is looking at the site. Each tick it:
- Finds the live round, or opens one.
- If the timer has expired, runs settlement.
- Otherwise, refreshes the live preview of pot size and player count.
Only one keeper
Two keepers would open competing rounds and could double-spend the pot. A unique partial index in the database makes a second one fail rather than succeed:
create unique index rounds_single_live_idx
on rounds ((status in ('open', 'drawing')))
where status in ('open', 'drawing');That constraint guarantees at most one round is accepting entries at any moment. It is a safety net, not a scaling strategy.
Restart safety
The keeper can be restarted at any point without invalidating a published commitment. Round seeds are not random values held in memory; they are derived deterministically from a master secret:
serverSeed = HMAC-SHA256(masterSecret, "gacha-stocks:round:" + roundNumber)
commitment = SHA256(serverSeed)A restarted keeper re-derives the same seed and honours the commitment it published before the restart. Revealing one round's seed tells an observer nothing about any other round, because the master secret never leaves the keeper.
If a keeper dies mid-draw, the next one finds the round stuck in the drawing state and retries it after a timeout rather than stranding it.
Failure handling
Every settlement step is wrapped. If anything throws, the round is marked void with the error recorded, and its SOL rolls into the next round. The failure modes it handles explicitly:
| Situation | Behaviour |
|---|---|
| Pot below the gas reserve or minimum | Void, roll the SOL forward |
| No eligible holders | Void, roll forward |
| Keeper wallet below its operating floor | Refuse to trade, void, roll forward |
| No route clears the price-impact cap | Fall back to the three cheapest routes, log a warning |
| Swap or payout transaction fails | Void with the error recorded, roll forward |
| Snapshot write fails after payout | Log and continue, the prize is already delivered |
Data it reads and writes
- Solana RPCfor balances, the holder snapshot, and sending transactions. The snapshot uses Helius's paginated token-accounts endpoint, with a provider-agnostic fallback.
- Meteora DBC for reading unclaimed fees and claiming them.
- Jupiter for routing quotes and the swap itself.
- Supabase for the round ledger, the entrant snapshots and the event log.
Dry run
Setting KEEPER_DRY_RUN=true simulates every transaction instead of sending it. Rounds still open, snapshot, roll and record, so the whole pipeline can be exercised against mainnet state without spending anything. A simulation that would fail raises rather than silently recording a fake success.
Preflight
Before going live, pnpm preflightchecks the database schema, RPC access, the keeper's balance against its floor, pool visibility, Jupiter routing, that the configured token decimals match the chain, and that every prize mint is still transferable. It exits non-zero if anything would stop a round settling.
The keeper is trusted to execute honestly, but not trusted to pick winners. That is what provable fairness is for: the draw itself is verifiable by anyone, independently of the operator.