# Triggair — full integration guide Triggair is a game backend for browser games. The `@triggair/sdk` client wraps it: one import, zero config beyond the publishable key, typed results, and errors that tell an agent how to fix the call. ## 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. Player-scoped calls attach it automatically. ## 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 (triggair_configure_leaderboard), then: await tg.leaderboards.submit('high_scores', 9000); const { entries } = await tg.leaderboards.top('high_scores', { limit: 10 }); ## 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(); Rewards are claimed from tg.inbox (the one hardened grant path). ## 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 only — no per-player data. ## 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 → 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 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/. ## 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. ## Error codes Every failure is a TriggairError `{ code, message, agentHint, requestId }` and links to /docs/errors/. ### bad_request (400) The request was malformed — a missing/invalid field, a bad slot/board/key name, or non-JSON body. Fix: Read the message: it names the offending field and its rule (e.g. slot 1–64 of A–Z a–z 0–9 _ -). Fix the input and resend. ### unauthorized (401) No valid credential. A game-scoped call needs the publishable key; a player-scoped call also needs a player token. Fix: Send `X-Triggair-Key: tg_pk_…`. For player calls, call login() first (the SDK does this automatically) so a `Authorization: Bearer ` is attached. If a token expired, the SDK refreshes and retries once. ### forbidden (403) Authenticated but not allowed — e.g. a banned player, or Turnstile is required for a new anonymous player. Fix: Do not retry a ban. For Turnstile, complete the challenge and resend `turnstile_token`. ### cors_forbidden (403) The browser Origin isn't in the game's allowed_origins. The classic 'works locally, 403s once deployed' trap. Fix: Add your deployed origin to allowed_origins (dashboard → CORS, or triggair_set_allowed_origins). An empty allowlist is open to any origin. ### conflict (409) The action conflicts with current state — e.g. a handle already taken, a friendship you've blocked, or a daily reward already claimed today. Fix: The message says which. Choose another handle, or don't re-claim the same day (daily resets on the server day boundary). ### save_conflict (409) An optimistic-concurrency (OCC) save conflict: the slot's version changed since you read it (a lost race or a stale If-Match). Fix: GET the slot to read the current version, merge your change, and PUT again with the new `ifMatch`. Omit ifMatch to opt into last-write-wins. ### not_found (404) The resource doesn't exist for this caller — an empty save slot, an unconfigured/disabled board, or a game/id you don't own. Fix: Check the id/slot/board. A board must be configured (triggair_configure_leaderboard) and enabled before scores work. Ownership 404s are uniform (no existence oracle). ### payload_too_large (413) The body exceeds a limit — a save > 256 KB, a config > 64 KB, or a share context > 4 KB. Fix: Shrink the payload (store only game-relevant state) or split across slots. The message names the cap. ### quota_exceeded (402) A plan limit was hit — save slots (3 on free), the monthly-active-player cap, or another metered ceiling. Fix: Free/reuse a resource (e.g. reuse a save slot) or upgrade the plan. This is not a transient retry — the request won't succeed until the limit clears. ### insufficient_funds (402) A wallet spend was denied because the balance would go negative. Balances never go negative — the whole transaction rolled back (you did not spend and fail to receive). Fix: Do NOT retry — the player genuinely can't afford it. Show the price gap, or route them to earn/buy the currency first, then try the purchase again with a NEW idempotency key. ### unknown_currency (400) A grant or spend referenced a currency that isn't defined for this game. Currencies are explicit definitions — there is no implicit create. Fix: Define the currency first (triggair_configure_currencies or the dashboard), then retry. Do not loop retrying the same undefined-currency call. ### unknown_item (400) A grant, consume, or store listing referenced an item that isn't in the game's catalog. Items are explicit definitions — there is no implicit create. Fix: Define the item first (triggair_configure_items or the dashboard), then retry. Do not loop retrying an undefined item. ### insufficient_item (409) A consume/spend asked for more of an item than the player owns. Inventory quantities never go negative — the whole transaction rolled back. Fix: Do NOT retry — the player doesn't have enough. Read the inventory (GET /inventory) and only consume what they own. ### store_limit_reached (403) The player already bought this listing up to its per-player purchase_limit. Fix: Do not retry — the limit is hit. Surface it in the UI (e.g. 'already purchased') rather than re-attempting the buy. ### out_of_stock (409) The store listing's global stock is exhausted. Fix: Do not retry — it's sold out. Mark the listing unavailable; it may restock later if the developer raises stock. ### rate_limited (429) Too many requests — the global token bucket or a per-board submission cap. A `Retry-After` header (and X-RateLimit-*) tells you when. Fix: Back off and retry after `Retry-After` seconds. The SDK does this automatically. Quote X-Request-Id if you report it. ### internal (500) An unexpected server error. Rare and transient. Fix: Retry with backoff (the SDK does). If it persists, report the request_id from the response so it can be traced.