HatchedDocs
Guides

Unlock gates

Spend primary tokens to unlock features — the non-cosmetic half of the token economy.

Unlock gates are how tokens get a meaning beyond dressing up the buddy. A gate is a named flag stored against a buddy; the user "unlocks" it by spending primary tokens. Whether it guards a premium feature, a higher difficulty, or a surprise reward is up to you.

The primitive is deliberately generic: Hatched stores the unlock, deducts the tokens, and guarantees idempotency. The client decides what the unlock means.

Create a gate

Gates are authored in the dashboard under Settings → Gates. Each gate has:

  • gate_key — stable identifier (e.g. advanced_mode, custom_skin_2). Snake_case recommended.
  • token_key — which primary token pays for it. Must match the customer's primary slot.
  • cost — positive integer.
  • metadata (optional) — arbitrary JSON returned to the client on lookup. Put display strings and feature flags here.

Gates live at the customer level, not per-buddy — every buddy can unlock the same gate once.

Unlock at runtime

import { HatchedClient, InsufficientBalanceError } from '@hatched/sdk-js';

const hatched = new HatchedClient({ apiKey: process.env.HATCHED_SECRET_KEY! });

try {
  const result = await hatched.gates.unlock(buddyId, 'advanced_mode');

  if (result.alreadyUnlocked) {
    // Idempotent — no tokens spent, existing unlock returned.
    console.log('Already unlocked at', result.unlock.unlockedAt);
  } else {
    // First unlock — tokens just got deducted.
    console.log('Unlocked for', result.gate.cost, result.gate.tokenKey);
  }
} catch (err) {
  if (err instanceof InsufficientBalanceError) {
    // User needs more tokens — show a nudge.
  } else {
    throw err;
  }
}

The call is idempotent: repeat calls return { alreadyUnlocked: true } without touching the ledger. That means you can retry safely on network failures, and you can call unlock() optimistically from a UI without double-spending.

List a buddy's unlocks

const unlocks = await hatched.gates.unlocks(buddyId);
// [
//   { gateKey: 'advanced_mode', unlockedAt: '2026-04-20T10:00:00Z', metadata: { ... } },
// ]

Typical usage: fetch once on app load, cache in the client, and treat it as the source of truth for which features to render.

List available gates

const gates = await hatched.gates.list();
// [
//   { gateKey: 'advanced_mode', tokenKey: 'gems', cost: 50, metadata: { label: 'Advanced mode' } },
// ]

Use this to render a "shop" of feature unlocks alongside the marketplace.

Publishable-key access

gates.unlock is scope-gated. A publishable key needs the write:unlocks scope granted explicitly — it is not part of the default scopes. That keeps browser-embedded clients from draining tokens without intent. gates.unlocks and gates.list are read-only and allowed under the default read:buddies scope.

Gotchas

  • Primary slot only. Gates can't spend progression tokens, by design (progression is monotonic). The dashboard refuses a gate pointing at the progression key.
  • No undo. There's no "refund" endpoint for an accidental unlock. Rename the gate key if you want to effectively reset (old unlocks remain attached to the dead key but the client treats them as stale).
  • alreadyUnlocked: true is normal. A client calling unlock() inside a useEffect on mount is a supported pattern — the second call is free.
  • Tokens — the two-tier model that backs gate costs.
  • Token economy — how the primary slot fits into spending.
  • Marketplace — the other primary-spent surface.