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: trueis normal. A client callingunlock()inside auseEffecton mount is a supported pattern — the second call is free.
Related
- Tokens — the two-tier model that backs gate costs.
- Token economy — how the primary slot fits into spending.
- Marketplace — the other primary-spent surface.