Token economy
How the two-tier token model, gates, and marketplace pricing fit together.
This page is the mental model for the whole token surface — how the two slots get seeded, where they're spent, and where they accumulate.
The loop
┌────────┐ events.send() ┌────────────┐
│ Event ├────────────────────▶│ Rule │
└────────┘ │ engine │
└─────┬──────┘
earn rules │ readiness conditions
┌──────────────────┼──────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ primary │◀─── buddies.spend │ progression │
│ (spendable) │ marketplace │ (earn-only) │
│ │ gates.unlock │ evolution gate │
└──────────────┘ └──────────────────┘Events feed both slots via the rule engine. The primary slot is where users consume; the progression slot is where the buddy grows.
How slots are seeded
When you apply an onboarding plan, Hatched looks at the token_config
bundle:
- If the plan already has two slots (one
primary, oneprogression) — it's used as-is. - If the plan is empty or partial — Hatched picks a theme from the plan description / target sector / creature style, then seeds both slots from a catalog.
The catalog matrix:
| Theme | Primary (spendable) | Progression (earn-only) |
|---|---|---|
fantasy | gems | mana |
fitness | reps | streaks |
corporate | points | xp |
education | stars | xp |
tech | bytes | commits |
default | coins | xp |
The resolved source lands in customers.settings.applied_sources:
{
"tokens": "plan" | "fallback",
"marketplace": "plan" | "fallback" | "hybrid",
"theme": "fantasy",
"applied_at": "2026-04-22T10:30:00Z"
}Check that field if you're debugging why a customer ended up with
gems/mana instead of the names in their plan.
Where each slot gets used
Primary is drawn from by:
hatched.buddies.spend(buddyId, { amount, reason })— direct spend.hatched.gates.unlock(buddyId, gateKey)— gate cost is always primary.- Marketplace item prices.
Progression is read by:
- Evolution readiness conditions (
token.<progression_key> >= N). - Any custom rule engine condition that references the balance.
- Dashboards, widgets, leaderboards.
Attempting buddies.spend({ token: '<progression_key>' }) returns
progression_not_spendable.
Why not one wallet
A single wallet forces an ugly trade-off: spending a coin on a hat would also lower the "how far I've come" number. Two slots lets the product feel consumerist (spend, trade) and progressive (never regress) at the same time.
If you genuinely want a single-wallet feel, set the primary token as your only visible balance and treat progression as a backend-only metric that drives evolution. Nothing in the SDK forces both to surface in the UI.
Migrating from the legacy 4-tuple
Customers created before 0.3 had a hardcoded hatch_token /
evolution_token / reroll_token / gift_token contract. Migration 024
added the kind column and defaulted every existing row to primary.
On the next onboarding apply, the fallback seeds a progression slot if
none exists — no manual step needed.
Related
- Tokens — slot contract.
- Marketplace — primary-priced catalog.
- Unlock gates — primary-spent feature flags.
- Evolution — progression-gated stages.