Daily chest — one claim per day, clock tricks be damned
Daily-reward double claims — via device-clock manipulation or plain request replay — are the most farmed exploit in free-to-play games, and "fixes" that compare timestamps keep losing to timezone math. The fix is not a better clock check. It is that each (player, day) claim is a one-way machine: unclaimed -> claimed, with `claimed` TERMINAL. The replayed request has no transition to fire — soma verify proves it. The coin ledger's >= 0 invariant keeps the wallet honest on the way through.
A Soma program from the games_economy part of the verified corpus: 57 lines, cells DailyChest, ChestTests. It passes soma check with soma 3.2.4.
How it is built
The cell DailyChest declares 2 signals in its face: claim_chest(player: String, day: String) -> Map and coins_of(player: String) -> Int; keeps 1 memory slot: coins (Map<String, Int>, persistent and consistent) under the invariant coins >= 0. Its state machine chest_claim starts in unclaimed and allows 1 transition: unclaimed -> claimed. claimed is terminal. Handlers: claim_chest (line 30) and coins_of (line 38).
The file also holds 1 test cell (ChestTests) with 9 assertions, run by soma test.
The source
daily_chest.cell
// ============================================================================
// DAILY CHEST — one claim per day, clock tricks be damned
// ============================================================================
// Daily-reward double claims — via device-clock manipulation or plain
// request replay — are the most farmed exploit in free-to-play games, and
// "fixes" that compare timestamps keep losing to timezone math. The fix is
// not a better clock check. It is that each (player, day) claim is a
// one-way machine: unclaimed -> claimed, with `claimed` TERMINAL. The
// replayed request has no transition to fire — soma verify proves it. The
// coin ledger's >= 0 invariant keeps the wallet honest on the way through.
// ============================================================================
cell DailyChest {
face {
signal claim_chest(player: String, day: String) -> Map
signal coins_of(player: String) -> Int
}
memory {
coins: Map<String, Int> [persistent, consistent]
invariant coins >= 0
}
// one instance per (player, day) — the smallest possible ratchet
state chest_claim {
initial: unclaimed
unclaimed -> claimed
}
on claim_chest(player: String, day: String) {
let key = "{player}@{day}"
let r = try { transition(key, "claimed") }
if r.error != () { return map("ok", false, "reason", "already claimed on {day}") }
coins.set(player, (coins.get(player) ?? 0) + 50)
return map("ok", true, "coins", coins.get(player))
}
on coins_of(player: String) { return coins.get(player) ?? 0 }
}
cell test ChestTests {
rules {
assert claim_chest("zoe", "2026-06-11").ok == true
assert coins_of("zoe") == 50
// THE REPLAY: same player, same day — the transition does not exist
assert claim_chest("zoe", "2026-06-11").ok == false
assert coins_of("zoe") == 50
assert_fails transition("zoe@2026-06-11", "claimed")
// a new day is a fresh machine instance, claimable exactly once
assert claim_chest("zoe", "2026-06-12").ok == true
assert coins_of("zoe") == 100
assert claim_chest("zoe", "2026-06-12").ok == false
// the wallet itself refuses corruption
assert_fails coins.set("zoe", -5)
}
}What the toolchain says
$ soma check daily_chest.cell
✓ All checks passed.$ soma test daily_chest.cell
test ChestTests ...
✓ assert claim_chest("zoe", "2026-06-11").ok == true
✓ assert coins_of("zoe") == 50
✓ assert claim_chest("zoe", "2026-06-11").ok == false
✓ assert coins_of("zoe") == 50
✓ assert_fails transition("zoe@2026-06-11", "claimed") — raised invalid transition: claimed → claimed. Current state: 'claimed'. Valid targets: []
✓ assert claim_chest("zoe", "2026-06-12").ok == true
✓ assert coins_of("zoe") == 100
✓ assert claim_chest("zoe", "2026-06-12").ok == false
✓ assert_fails coins.set("zoe", -5) — raised memory invariant violated on 'coins': coins >= 0 — rejected write of -5 (key "zoe"); the slot is unchanged
9 tests: 9 passed, 0 failed$ soma verify daily_chest.cell
Verifying daily_chest.cell...
State machine 'chest_claim': 2 states, initial 'unclaimed'
States: [claimed, unclaimed]
✓ 2 states, 1 transitions
✓ all states reachable from 'unclaimed'
✓ terminal states: [claimed]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
✓ think-isolated: CTL safety properties hold regardless of LLM output (1 handlers, 1 literal transitions, 0 dynamic)
✓ termination: all 2 handlers structurally terminate
✓ refinement: handler `claim_chest` ⟶ {claimed}
· transition sources: `claim_chest` calls transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
DailyChest/invariants:
✓ invariant coins >= 0 — writer 'claim_chest' proven by induction (writes (coins.get(player) ?? 0) + 50)
9 passed, 0 warnings, 0 failures
Temporal properties for 'chest_claim':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK