Systems/Mineral Economy
04 / 06

Mineral Economy

Olivium, Ruby, Emerald, Sapphire

EconomyLedgerDecay

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

1

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.

2

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.

3

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.

4

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.

5

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.