Cranking#
Seesaw lifecycle transitions are permissionless. A cranker is any process or user that submits the transaction needed to move a market forward after the relevant time boundary.
Solana Lifecycle Cranks#
| Transition | When to call | Result |
|---|---|---|
snapshot_end | After market.t_end when the end Pyth price is fresh | Captures the closing price and cancels open orders |
resolve_market | After the end snapshot exists | Sets the market outcome from start and end prices |
expire_market | After the configured expiry window if resolution cannot complete | Marks the market expired for 50/50 settlement |
claim_creator_fees | After resolution or expiry when deferred creator fees are non-zero | Sends accumulated creator fees to the market creator |
close_market | After the close delay and once all settlement liabilities are drained | Closes the market account and returns rent |
Operator Notes#
- Crank transactions must use the same market PDA derivation as clients: feed id, duration, epoch id, and creator.
- Oracle cranks should wait for the first Pyth update with
publish_time >= boundary, not the nearest or averaged price. max_confidence_ratio_bps = 0disables the confidence-ratio guard for that market.max_oracle_jump_bps = 0selects the protocol default rather than disabling the jump guard.- Crank workers should be idempotent. A market may already have moved forward by the time a submitted transaction lands.
Product And Fallback Policy#
- Backend crank workers are the production path for
SnapshotEnd,ResolveMarket,ExpireMarket, deferred creator-fee claim, andCloseMarket. - Web exposes pro/manual lifecycle controls as an operator fallback. These are for stuck-market recovery and smoke testing, not the primary retail path.
- Mobile intentionally does not expose lifecycle cranks. If backend cranking is unhealthy, mobile should show stale/settling status while operators use backend, CLI, or web pro controls.
- The indexer should alert on markets that remain:
Tradingaftert_end + 2 minutes;- snapshotted/unresolved after
t_end + 5 minutes; - unresolved and inside the expiry window with no successful
ExpireMarketattempt; - resolved/expired with deferred creator fees for more than one crank interval;
- close-eligible but still open after the close delay plus one crank interval.
- Before broad beta, run an RPC-backed lifecycle smoke test for both the normal
SnapshotEnd -> ResolveMarket -> ClaimCreatorFees -> CloseMarketpath and theExpireMarketpath.
Pull Keeper Monitoring#
For Pull markets, Seesaw's default pyth-keeper is the production cranker, but
it is not privileged. It watches Pull markets, posts guardian-signed Pyth updates
through the Pyth Receiver, and calls snapshot_end, resolve_market, or
expire_market when the market state allows it. Any other caller can do the same
with the correct market accounts and a valid Receiver-owned PriceUpdateV2
account.
Keeper Redundancy Policy#
Launch operations must run at least two independent Pull keeper instances before enabling Pull markets for users:
- Primary keeper: Seesaw-operated production infrastructure.
- Secondary keeper: separate infrastructure, separate RPC provider or endpoint, separate keeper keypair, and its own alert route.
Both keepers may watch the same markets. Lifecycle instructions are permissionless and idempotent, so duplicate attempts are expected: the first confirmed transaction advances the market, and the later attempt either no-ops or fails without changing the settled outcome. Do not give either keeper special authority; redundancy is operational liveness, not a trust boundary.
A single-keeper deployment has one operational failure mode: markets can remain
stuck after t_end if that process, RPC endpoint, relay, wallet balance, or
alert route fails. User funds remain in the program, but traders see delayed
resolution, claim, and close flows until any operator submits the same crank with
the keeper, CLI, SDK, or web pro controls.
Launch signoff must record both keeper /readiness payloads, wallet balances,
image digests, target cluster/program id, alert owners, and the result of a
supervised duplicate-crank smoke test where one keeper wins and the other handles
the already-advanced market cleanly.
Do not mark this path production-ready from process uptime alone. A complete monitoring setup must prove both that the keeper is ticking and that markets are actually advancing.
Keeper process checks:
| Check | Required alert |
|---|---|
/health or /readiness unavailable | Alert after more than two poll intervals |
keeper.dryRun from /readiness | Alert immediately if true in production |
lastTickUnix | Alert when older than max(60s, 3 × poll interval) |
consecutiveTickErrors | Alert when non-zero for more than two poll intervals |
actionsFailed from /metrics | Alert when increasing across checks |
| Keeper SOL balance | Alert below 0.01 SOL; refill before transaction fees or Pyth-update rent fail |
Market-state checks:
| Check | Required alert |
|---|---|
Pull market still Trading | More than two minutes after t_end |
Pull market has no end_price | Within five minutes of t_end + market_expiration_window_seconds |
Pull market unresolved with no end_price | At or after t_end + market_expiration_window_seconds |
| Pull market resolved/expired with deferred creator fees | More than one crank interval |
The keeper exposes health, JSON, and Prometheus endpoints when METRICS_PORT is set:
curl -fsS http://127.0.0.1:9091/health
curl -fsS http://127.0.0.1:9091/readiness
curl -fsS http://127.0.0.1:9091/metrics
curl -fsS http://127.0.0.1:9091/metrics/prometheus
Use a process supervisor such as Docker --restart unless-stopped, systemd, or
Kubernetes restart policy, but pair it with the alerts above. A container can be
up while every action fails because the relay is unreachable, the RPC endpoint is
degraded, DRY_RUN=true, the keeper wallet is unfunded or below
MIN_WALLET_BALANCE_LAMPORTS, or the Pyth/Hermes API key on the relay has
expired.
The public operator guide and open-source implementation are:
CLI Fallback#
The CLI exposes the same lifecycle cranks for operators who need to bypass app UI. Use the concrete market/feed/creator values from the indexer or market creation logs:
seesaw market snapshot \
--market-id <epoch-id> \
--pyth-feed <pyth-price-account> \
--pyth-feed-id <32-byte-feed-id-hex> \
--creator <creator-address>
seesaw market resolve \
--market-id <epoch-id> \
--pyth-feed-id <32-byte-feed-id-hex> \
--creator <creator-address>
seesaw market expire \
--market-id <epoch-id> \
--pyth-feed-id <32-byte-feed-id-hex> \
--creator <creator-address>