triggair
Recipes

Copy-paste integration recipes

Each recipe is a self-contained snippet you (or your agent) can lift verbatim. They are the same building blocks that assemble llms-full.txt.

Install & initialize

npm i @triggair/sdk
import { createClient } from '@triggair/sdk';
const tg = createClient({ key: 'tg_pk_your_key' });
Only the publishable key (tg_pk_) is needed. NEVER put a secret key (tg_sk_) in client code — it belongs on a server.

Player identity (anonymous-first)

const { playerId } = await tg.login();
An anonymous player token is minted from a device id on first use and silently refreshed. Every player-scoped call attaches it automatically — no accounts, no login screen required.

Cloud saves

await tg.saves.put('slot1', { level: 4 });        // last-write-wins
await tg.saves.put('slot1', data, { ifMatch: v });  // conflict-safe (throws save_conflict)
const { data } = await tg.saves.get('slot1');
tg.saves.queue('slot1', data);                     // durable/offline (replays on reconnect)

Leaderboards

Configure a board once (dashboard → Leaderboards, or triggair_configure_leaderboard), then:
await tg.leaderboards.submit('high_scores', 9000);
const { entries } = await tg.leaderboards.top('high_scores', { limit: 10 });
Keyed boards (per-team / per-UGC-item) live on tg.keyedBoards.

Player stats & profile

await tg.stats.update([{ key: 'kills', op: 'increment', value: 1 }]);
const stats = await tg.stats.get();   // the player's own stats
Stats back leaderboards, achievements, and segment targeting.

Achievements & daily rewards

await tg.achievements.report('first_win', 1);   // reward lands in the inbox on unlock
const status = await tg.daily.status();
if (status.claimable) await tg.daily.claim();     // server-day gated; streaks tracked
All rewards are collected from tg.inbox — the one hardened grant path.

Inbox & reward claims

const items = await tg.inbox.list();
for (const it of items) if (it.claimable) await tg.inbox.claim(it.id);
Claiming is exactly-once (idempotent per item) — the single audited path that grants currency, items, or stat rewards.

Economy: currency, store, inventory

const wallet = await tg.economy.wallet();
await tg.economy.buy('main_store', listingId, { idem: uuid });  // server-authoritative price + balance
const items = await tg.economy.inventory();
await tg.economy.equip(itemId);
Grants are server-side and idempotent (pass idem); insufficient_funds when the player can't afford it.

Loot boxes & energy

const odds = await tg.economy.loot.odds('bronze_box');  // disclosed drop rates
const result = await tg.economy.loot.open('bronze_box', { idem: uuid });
await tg.economy.energy.spend('stamina', 1, { idem: uuid });   // regenerating meter
Loot/IAP are age-gated where required — see the age-gate recipe.

Remote config, flags & live events

const cfg = await tg.config.get();        // your published key→value blob
const live = await tg.config.liveEvents();
Feature flags (tg.flags) resolve per-segment with a break-glass kill switch; edit config/flags in the dashboard (Setup → Config, Operations → LiveOps) or via MCP — no client deploy.

Moderation (names, chat, UGC)

const v = await tg.moderation.check('username', name);   // allow | mask | block | review
if (v.verdict === 'block') { /* reject */ }
await tg.moderation.report('player', targetId, 'harassment');
const me = await tg.moderation.myStatus();   // the caller's bans/mutes (shadow hidden)
Run moderation BEFORE anything is visible — it defeats leet/homoglyph evasion.

Analytics (durable & offline)

tg.track('level_complete');
Events are coalesced and queued in a durable outbox; they survive a dropped connection and flush on reconnect. Counts + funnels + retention + sessions roll up nightly — view them in the dashboard Analytics tab. No per-player PII.

Realtime rooms (presence & chat)

const room = await tg.realtime.join('lobby');   // authed WebSocket; resolves on connect
room.on('presence', ({ members }) => renderRoster(members));
room.on('message', ({ from, data }) => renderChat(from, data));
room.send({ text: 'gg' });      // fan-out to everyone else in the room
room.close();
Presence + broadcast over a Durable Object; `room.members` is the live roster; `room.history` replays recent chat on join. Chat text is moderated in transit (mask/block) by the game's policy. Private rooms: join('team:<teamId>') requires team membership, join('match:<matchId>') requires being a participant (non-members get 403); any other name is public. Node/tests pass a WebSocket via createClient({ webSocket }).

Crash reporting

try { /* game loop */ } catch (e) {
  tg.crashes.report(e.message, { stack: e.stack, appVersion: '1.4.0' });
}
Crashes are grouped server-side by a normalized-stack fingerprint into a handful of issues with a crash-free-users %.

Turn-based matches

const { match } = await tg.asyncMatch.create({ type: 'chess', opponents: [otherId] });
await tg.asyncMatch.turn(match.id, { state: next, version: match.version });
Server-enforced turn order (not_your_turn) + OCC (async_conflict). No realtime connection needed — players move whenever.

Storage collections

await tg.storage.put('decks', 'main', { cards: [] });
const { data } = await tg.storage.get('decks', 'main');
await tg.storage.incr('stats', 'wins', 1);   // atomic, avoids read-modify-write races
Scopes: player (private), shared (game-wide), team (members only). Configure collections in the dashboard.

Promo codes & gifts

await tg.codes.redeem('LAUNCH2026');   // grants the campaign reward into the inbox
await tg.economy.gifts.send(friendId, { item: 'sword', qty: 1 }, { idem: uuid });
code_invalid / code_expired / code_already_redeemed guard redemption.

Tournaments (scheduled, prize tables)

const opens = await tg.tournaments.list();      // status: scheduled | running | closed
await tg.tournaments.join(t.id);                // may charge entry_fee → { joined, reason?, fee_txn }
await tg.leaderboards.submit(t.board, score);   // you score via the tournament's own board
const top = await tg.tournaments.standings(t.id, { limit: 20 });
const me = await tg.tournaments.me(t.id);       // { entered, rank, prize }
Prizes land in the inbox when the operator closes it. Configure schedule + reward_table in the dashboard/MCP.

Leagues (tiered divisions, promotion/relegation)

await tg.leagues.join('ranked');                // placed in the lowest division for the season (idempotent)
const me = await tg.leagues.me('ranked');       // { division, division_name, rank, zone: promoting|safe|relegating }
const standings = await tg.leagues.divisionTop('ranked', me.division);
Players score through tg.leaderboards.submit (the league ranks that board); promotion/relegation runs on the operator-driven season advance.

Teams & clans

const { team } = await tg.teams.create('Night Owls', 'OWL', { privacy: 'invite_only' });
await tg.teams.join(teamId);                    // open teams; invite_only → tg.teams.requestJoin(teamId)
await tg.teams.invite(teamId, playerId);        // admin/owner; recipient calls tg.teams.acceptInvite(inviteId)
const t = await tg.teams.get(teamId);           // team + roster
const board = await tg.teams.leaderboard('weekly', { agg: 'sum' });   // members' scores aggregated
Admin actions are role-gated server-side: setRole · kick · transfer · ban · disband. Errors: team_forbidden (role), team_full (cap), conflict (tag taken).

Keyed leaderboards (per-entity boards)

await tg.keyedBoards.submit('level_times', levelId, 42.5);   // score keyed to any entity (a UGC level, a team, a map)
const top = await tg.keyedBoards.top('level_times', { limit: 10 });
const mine = await tg.keyedBoards.entry('level_times', levelId);
Use these when the ranked subject isn't the player — one board namespace, many entities. Configure the board in the dashboard.

Battle pass & quests

const bp = await tg.battlePass.get('s3');       // { bp, tier, claimed lanes }
await tg.battlePass.claim('s3', tier, 'premium');   // reward → inbox; tier_not_earned / premium_required guard it
const quests = await tg.quests.list();          // progress per objective
await tg.quests.claim(questKey);                // when complete → reward lands in the inbox
Progress is driven by tg.stats.update / achievements; all rewards are collected from tg.inbox.

Player segments

const segments = await tg.segments.mine();      // [{ key, segment_id, source }]
Segments materialize from stats/behaviour and are DEFINED in the dashboard (authoring, not client). When a player token is present they automatically personalize tg.config.get() and tg.flags — read mine() only if you want to branch UI on membership.

User-generated content (create, publish, remix)

const { item } = await tg.ugc.create('level', { title: 'Sky Temple', payload, allow_remix: true });
await tg.ugc.submit(item.id);                   // runs moderation → published or held for review
const feed = await tg.ugc.browse({ sort: 'popular' });   // new | top | popular
await tg.ugc.play(id);                          // debounced play count; unlocks rating
await tg.ugc.rate(id, 5); await tg.ugc.like(id);
const { item: fork } = await tg.ugc.remix(id);  // fork a remix-allowed item → new draft with lineage/attribution
const tree = await tg.ugc.lineage(id);          // parent (remix_of) + root_id + remixes

More domains, same shape

Every domain is configured in the dashboard/MCP, then called on the typed SDK client with the same error contract: tg.progression (level curve/XP) · tg.storage (collections) · tg.codes (promo codes) · tg.asyncMatch (turn-based) · tg.crashes · tg.rng. See /docs/guides for the narrative walkthrough of each.

CORS: works locally, 403s when deployed

Browser calls are origin-checked. If a call fails only once deployed, add your deployed origin to the game's allowed_origins (dashboard → Setup → Keys/CORS, or triggair_set_allowed_origins). An empty allowlist = open to any origin — set it before you ship.

Recovery (cross-device)

const { code } = await tg.mintRecoveryCode();  // show/share once
await tg.recover(code);                        // same anonymous player on a new device

Errors are actionable

Every failure throws a TriggairError with { code, message, agentHint, requestId }. Read agentHint — it tells you how to fix the call. rate_limited/5xx/network retry automatically. See /docs/errors/<code>.

Verify your integration

After wiring the SDK, run the MCP tool triggair_verify_integration (or the dashboard's verify runner). It live-probes player + saves + leaderboards and advises on CORS, with pass/fail per service and fix hints — closing the loop.