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.
Age gate & parental consent (compliance)
await tg.compliance.setAge({ birthYear: 2013 }); // mapped to a bracket + discarded (no DOB stored)
const view = await tg.compliance.status(); // { bracket, consent_state, gated: {feature:bool} }
if (view.gated.open_chat) { /* disable in UI; the server also enforces it */ }
await tg.compliance.requestConsent('parent@email'); // emails the parent a signed decision link Gate regulated features (chat, loot/IAP) with view.gated; the server is the enforcement.
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.