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' });

No build step (single-file HTML)? Import from a CDN instead, no npm needed:

<script type="module">
  import { createClient } from 'https://esm.sh/@triggair/sdk@1'; // pin @1 or an exact version
  const tg = createClient({ key: 'tg_pk_your_key' });
</script>

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, so there are no accounts and no login screen to build. Want durable cross-device login (email/password)? See the Player accounts recipe.

Player accounts (login across devices)

Optional durable identity on top of anonymous-first: a player signs in and gets the SAME player on every device. Enable it per game in the dashboard (Setup -> Keys -> Player accounts); anonymous play always keeps working, so accounts are an upgrade, not a wall.

await tg.auth.providers();                                   // ['password','google'] or [] when off
const { needsConfirmation } = await tg.auth.signUp(email, password);   // emails a confirmation link
const r = await tg.auth.signInWithPassword(email, password);
await tg.auth.signInWithGoogle();                           // popup OAuth; call from a click. Same outcomes/merge
// r.outcome: 'linked' (first login carried this device's anonymous progress) | 'resumed' | 'created' | 'conflict'
if (r.merge) await tg.auth.resolveMerge('keep_account');     // or 'use_anonymous' to bring THIS device's progress in
tg.auth.onIdentityChanged(() => refetchPlayerData());        // the active player id may have switched
await tg.auth.sendPasswordReset(email);
await tg.auth.signOut();                                     // clears the session, back to a fresh anonymous player

First sign-in LINKS the current anonymous player (progress follows the account). If BOTH the account and this device already have save data, outcome is 'conflict' and you pick which wins via resolveMerge; the losing player is parked (never deleted), not overwritten. The credential layer never touches your game data: the SDK still uses the same per-game player token for every call.

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, with no client deploy.

A/B experiments

const { variant } = await tg.experiments.assign('checkout_cta');  // deterministic + sticky
if (variant === 'green') showGreen(); else showDefault();          // null ⇒ control (unknown/not-running)
await tg.experiments.track('checkout_cta', 'purchase');           // record the conversion

Define variants + weights + the metric in the dashboard (Operations → LiveOps) or via MCP; only a `running` experiment assigns. Per-variant exposures/conversions/rate are on the results view; mind the sample size before calling a winner.

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.

Web push notifications

await navigator.serviceWorker.register('/sw.js');
await tg.push.subscribe();   // fetches the VAPID key + registers the browser subscription (adults only — behavioral_push gate)
// SW: self.addEventListener('push', e => { const n = e.data.json(); self.registration.showNotification(n.title, { body: n.body, data: n }); });

Send from the dashboard Push panel or the triggair_send_push MCP tool to target all / a segment / one player. Self-hosted VAPID, no vendor. Dead subscriptions are pruned on send.

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.on('typing', ({ player, state }) => showTyping(player, state));
room.on('reconnecting', ({ attempt }) => setStatus('reconnecting'));
room.on('reconnected', () => setStatus('live'));
room.send({ text: 'gg' });      // fan-out to everyone else
room.typing(true);              // ephemeral indicator (not stored/moderated)

Presence + broadcast over a live connection; `room.members` is the live roster; `room.history` replays recent chat on join. The connection SELF-HEALS (auto-reconnect + backoff + heartbeat). Chat text is moderated in transit (mask/block). Private rooms: join('team:<teamId>') requires team membership, join('match:<matchId>') requires being a participant (non-members get 403); any other name is public. History size is per-game (triggair_configure_realtime, 0 = opt-out). 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 { value } = 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, so 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

This is the no-signup rescue path. For a real login (email/password across devices), see the Player accounts recipe.

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.