HatchedDocs
Concepts

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, one progression) — 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:

ThemePrimary (spendable)Progression (earn-only)
fantasygemsmana
fitnessrepsstreaks
corporatepointsxp
educationstarsxp
techbytescommits
defaultcoinsxp

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.