Roles
Keeper
The keeper calls the permissionless transitions that follow a deadline, so that markets open, settle and pay out on time; what it earns and risks, and the calls it makes.
Every state change that follows a deadline is permissionless: anyone may call it, and the contracts check only the state and the clock. A keeper is any agent that does so. It has no authority, cannot change a result and holds no role. Reference implementation: examples/agents/src/roles/keeper.ts.
Duties
Each tick the reference keeper lists the markets in each live state and calls whichever transition is due, judged by the latest block time:
- Open a market after its creation window:
openon aPENDINGmarket whoseopenAthas passed and that is not creation-challenged. - Finalize an unchallenged proposal after its challenge window:
finalizeUnchallengedon aPROPOSEDmarket whose proposal'schallengeEndhas passed. - Time out a market that is not final at its
hardDeadline:finalizeTimeoutin any non-final state. The outcome becomes INVALID, and each share on either side pays half. - Settle a dispute round once its reveal phase is over:
settleRound. A round that misses quorum or ties restarts instead of settling; it is not INVALID. - Finalize a dispute once the settled round's appeal window has passed:
finalizeDispute. - Distribute the fees of a
FINALmarket:distributeFees, once per market. - Anchor forecast roots. For every closed forecast window whose root
BlinkAnchordoes not hold, callanchor(root);anchoredAt(root)says whether it is held. - Lose races quietly. Several keepers may run at once. A transaction that loses to another keeper (
InvalidState,AlreadySettled,AlreadyDistributedorAlreadyAnchored) is logged once as{"event":"raced"}and the keeper moves on. Any other failure propagates, so a429or a stale index reaches the loop's backoff instead of becoming a retry storm.
Stake or bond
None. A keeper holds no role and posts no stake or bond, and nothing on chain requires it to register. The reference keeper is onboarded like every reference agent (it registers and gets an API key), but it only uses public reads.
Income
- Execution tip.
distributeFeespays the callerkeeperTipBpsof the resolution share of the fee pool. That tip is the only payment a keeper receives. - The other transitions pay nothing, so they cost gas and nothing more. A market that no keeper moves still times out, because anyone, including a trader or a creator, can send the call.
Credit
Not counted. A keeper has no on-chain credit and needs none.
Risks
- Gas for nothing. A transaction that loses a race still costs gas. Check the state and the time before you send, and expect to lose some.
- Chain time, not yours. Every deadline is a block timestamp. Compare with the latest block, not the host clock.
- A stale or limited API. The keeper reads market lists and disputes from the API. On
503 INDEX_STALEor429it must wait forRetry-Afterand act on no stale read; the chain stays the source of truth for any call. - Polling cost. The reference keeper reads one list per live state each tick, ten pages of 100 at most; a deployment with more markets in one state needs a cursor kept in state. A final market with a window that has no root is scanned again each tick.
- No power over outcomes. A keeper cannot choose a result, skip a deadline or change a payout; it only supplies the call.
- Anchoring is optional. Anchoring a root makes its first-seen time public; it does not change any score.
Steps
The order below is one tick of the reference keeper (examples/agents/src/roles/keeper.ts): the states PENDING, OPEN, PROPOSED, DISPUTED and FINAL, in that order.
GET /v1/deployment(clientgetDeployment()): read the addresses, the anchor address andparams(keeperTipBps).- Onboard as in Onboarding if you want an API key; no role is needed.
- For each state:
GET /v1/markets(clientlistMarkets), and for each market in a state beforeFINAL:- If its
hardDeadlinehas passed:BlinkResolution.finalizeTimeout(id), and skip the rest of this list for the market. PENDINGwith a creation challenge: drive its dispute (below).PENDINGwith no challenge andopenAtpassed:BlinkMarkets.open(id).PROPOSED:GET /v1/markets/{id}(clientgetMarket) returns the proposal and itschallengeEnd; after it,BlinkResolution.finalizeUnchallenged(id).DISPUTED: drive its dispute.
- If its
- Drive a dispute:
GET /v1/markets/{id}/disputes(clientlistDisputes), thenBlinkResolution.getDispute(disputeId)for the current round. AfterrevealEnd,BlinkResolution.settleRound(disputeId). AfterappealEndof a settled round that did not restart,BlinkResolution.finalizeDispute(disputeId). - For a
FINALmarket whose fees are not distributed:BlinkMarkets.distributeFees(id). - For every market that is not
PENDING, and not yet recorded as done (including one just timed out in step 3, in the same tick):GET /v1/markets/{id}/forecast-windows(clientlistForecastWindows) lists each window'sroot; ifBlinkAnchor.anchoredAt(root)is zero, callBlinkAnchor.anchor(root).