Mineral Economy
Olivium, Ruby, Emerald, Sapphire
Overview
The mineral system is a closed economy that runs entirely server-side. The `mineralLedger` table is append-only: every grant writes a row with `delta` and `balanceAfter`, and the current balance is the latest balanceAfter for that user + rockType.
How it works
Grants: `goals.update` with `status: 'done'` triggers a server-side check — if the goal qualifies, a `minerals.grant` action writes ledger rows and returns the deltas to the client, which surfaces them via the OliviumToast.
Debits: spending Olivium on a break or club join runs through `minerals.debit`, which validates balance, writes the ledger row, and updates `mineralBalances` on the user.
Decay: a Convex scheduled function runs nightly. For each user not currently on a break, it computes the day delta and writes negative ledger rows.
First-earn modal: each user has `mineralFirstEarnedAt` timestamps. When a grant flips a timestamp from null to set, the client surfaces a celebratory takeover the next time they open the app.
Break: `minerals.startBreak` debits Olivium and sets `breakUntilAt` to now + duration. While breakUntilAt > now, decay skips this user.
Key decisions
Append-only ledger over mutable balances
We store balances on the user row for fast reads, but the ledger is the source of truth. Discrepancies are detectable; reversals are writable as inverse rows. The ledger powers the vault's recent-activity list for free.
Olivium as the only spendable mineral
Three of the four minerals can't be spent yet. That's intentional — they accumulate. The economic complexity from multiple spendables is real, and we'd rather grow the Olivium loops first.
Decay only when not breaking
Decay is the loss function that makes consistency matter. But raw decay punishes people who legitimately step away — for vacations, illness, etc. So we sell breaks: spend Olivium, pause decay for a window. The agent stays in control.