Appearance
Chip Claim Board
What was decided
The beta's play money (playSKPK) is earned, not faucet-poured. A server-authoritative, config-driven claim board grants playSKPK for real actions: a weekly 500 baseline + one-time escalating unlock sections (connect wallet, build an agent, seat it, inspect a decision, tune & republish). It replaces the faucet.
Four decisions carry the design:
Three tables, three roles — never conflated.
claim_sectionsis the env-scoped catalog (config, not code — an operator edits amounts / conditions / order / enabled with no deploy;'all'is the default tier, a concrete-environment row overrides it, mirroringgame_settings).user_claim_sectionsis per-user state (locked → lit → claimed; baseline claimability is derived from a stored period vs the current ISO-week, never stored).claim_ledgeris the immutable audit — itsUNIQUE claim_idis the DB-level no-double-credit floor.One generic seam — sources fire condition KEYS, never section state.
markConditionMet(userId, conditionKey, idempotencyKey)lights every enabled catalog section bound to that key. Config decides which keys pay and how much, so adding a reward source is a catalog row + a new key with zero engine change (proven by A14 — a novel key lights a seeded section; the_fixturecanary documents the shape).The claim is server-rechecked in a transaction, then mints once. Identity is JWT-only (never a body
userId). Inside one tx: re-read the live config + kill-switch, deriveclaim_id = keccak256(userId:sectionKey:period), verify the section is genuinely claimable (alitunlock or a period-fresh baseline), then mint via the cage playSKPK grant to the user's linked Solana wallet and writeclaimed+ the ledger row. No-free-mint (locked / already-claimed / UI-bypass → nothing) and no-double-mint (the ledgerclaim_idUNIQUE + the cage's on-chaingrant_markerPDA) are the fund-safety spine.The board UI is config-driven; the claim action is affordance-gated.
ClaimBoard.tsxrenders whatever the catalog returns — never a hardcoded section list, so a config-added section appears with no code change; a claim button exists only whensection.claimable. State is server-authoritative; the UI reflects it, never drives it. A corner progress-tracker widget (bottom-left, collapsible, dismissible) surfaces the unlock ladder on every page.
Key tradeoffs accepted
- The whole feature deploys dark. A
claim_board_enabledgame_settingsglobal (fail-closed OFF, resolved per-environment) gates the board,markConditionMet, and the claim endpoint — turning it on in prod is a flag flip, not a deploy (thebetting_enabledprecedent). - No stacking, derived from the clock. The weekly baseline credits its window amount once per window — skip three weeks, still get 500. Claimability comes from the process clock at claim time, not a cron; a strict DB-
now()derivation is a trivial refinement if multiple game-servers ever need window-boundary consensus. - The mint fires inside the claim tx. The grant is awaited before the ledger +
claimedwrite, so a no-wallet user gets a fail-closed empty commit (link a wallet, retry) and a cage/on-chain failure rolls the tx back — never claimed-without-mint. - The operator surface is raw (U1). A minimal admin row editor over
/admin/claim-sections, not a polished config UX — the catalog is the source of truth and the UX can grow later. kind/cadenceare immutable once a key exists (falsification A16). An operator edits amount/condition/order/enabled freely, but akind/cadencechange on an existing key is refused (409 kind_cadence_immutable) — those two determine theperiodinside the claimId that the no-double-mint spine keys on, so flipping a claimedunlockto abaselinewould re-open it for a second mint. Backed at the claim path by an "unlock is once-ever per user" floor (blocks on any prior ledger row for the key), so reconfiguration or delete-recreate can't re-open a paid unlock.- Supersedes the faucet +
faucet_overrides(invite-faucet-admin); invite codes stay. SKUSDC / wrap /exchange-uiare dropped from the beta.
Verified
The full loop runs against a live solana-test-validator (Solana profile, real cage grant): baseline credit + weekly re-arm + no-stacking (A2/A3/A4), lit-unlock credit + no-double-mint (A6/A8/A15), no-free-mint on locked / UI-bypass (A7/A9), config re-tune / add / disable with no deploy (A11/A12/A13/A14), and the full server span chain claim.condition_met → claim.attempt → claim.grant on one claim — 14/14 integration. The board + widget are RTL-covered (7/7), and the operator GET/PATCH/DELETE surface is smoked end-to-end (6 sections, no-key 403, upsert, delete).
See the full ADR in the archive: .indusk/planning/archive/chip-claim-board/adr.md, and the Play Money & the Claim Board guide.