# GammaSDK — full documentation > Official SDK and developer docs for building HTML5 games on Gamma Games — coins, cloud save, player profiles, and a canonical coin value. Package: @swiftware/gamma-sdk. # Introduction # GammaSDK **GammaSDK** lets your HTML5 game talk to **Gamma Games** — the player's coins, cloud-synced save data, public profile, and the canonical Gamma Coin value. There are two ways to integrate, and this site covers both: - **The package** — [`@swiftware/gamma-sdk`](https://www.npmjs.com/package/@swiftware/gamma-sdk), a tiny, typed, promise-first wrapper. **Recommended for almost everyone.** It handles ready-waiting, the offline fallback, and ships a dev mock so you can build and test outside the app. - **Manual** — call the raw `window.GammaSDK` bridge directly, no build step. See [Manual integration](./manual-integration.md). ## What you can do | Capability | Package | Notes | |---|---|---| | Read / spend / earn coins | `getCoins`, `spendCoins`, `earnCoins` | Server-authoritative, atomic | | Cloud save | `gamma.storage` | Account-bound, `localStorage` fallback | | Player profile | `getPlayer` | Public fields only | | Canonical coin value | `getCoinValue`, `coinsToUsd`, `formatCoins` | Shared by every game | ## The golden rule > **Coins only work inside the Gamma Games app.** That's where the real, > server-authoritative bridge is injected. Outside the app the SDK is **inert** by > default — guard coin features on `gamma.isInApp` and keep an offline path. See > [App-only & offline](./guides/app-only-and-offline.md). ## Next steps - [Install](./getting-started/install.md) the package or drop-in script. - [Quickstart](./getting-started/quickstart.md) — integrate in 3 steps. - [Integrate with AI](./ai-integration.mdx) — copy one prompt into your AI tool. --- # Install # Install ## With a bundler (npm) ```bash npm install @swiftware/gamma-sdk ``` ```js import {gamma} from '@swiftware/gamma-sdk'; ``` The package ships ESM, CJS, and TypeScript types — works with Vite, webpack, Rollup, esbuild, Next.js, etc. ## No build step (drop-in script) If your game is plain HTML/JS with no bundler, use the single drop-in file. It exposes a global `gamma`: ```html ``` You can also self-host the file — copy `dist/gamma-sdk.min.js` from the package next to your game and reference it locally. ## Requirements - Any modern browser. The SDK is ~6 KB minified. - Host app **GammaSDK ≥ 1.1.0** for `getCoinValue` (older hosts fall back to sensible defaults automatically). Next: [Quickstart →](./quickstart.md) --- # Quickstart # Quickstart Integrate the SDK in three steps. ```js import {gamma} from '@swiftware/gamma-sdk'; // 1. Wait until the SDK has settled (real host in-app, or fallback outside it). await gamma.ready(); // 2. Load the player's save (account-bound in-app, localStorage outside). await gamma.storage.init(); const state = (await gamma.storage.load()) ?? {schemaVersion: 1}; // 3. Use coins / save progress. Only grant a reward when `ok` is true. const res = await gamma.spendCoins(50, 'extra_life'); if (res.ok) { grantExtraLife(); } await gamma.storage.save(state); ``` That's the whole happy path. The rest of the docs are detail. ## A more complete boot sequence ```js import {gamma} from '@swiftware/gamma-sdk'; async function boot() { await gamma.ready(); if (gamma.isInApp) { const player = await gamma.getPlayer(); if (player) greet(player.displayName); } await gamma.storage.init(); const save = (await gamma.storage.load()) ?? defaultState(); applyState(save); startGame(); } boot(); ``` ## Things to remember - **Coins only work inside the Gamma Games app.** Outside it, coin calls return `{ok: false, error: 'no_transport'}`. Gate features on `gamma.isInApp`. See [App-only & offline](../guides/app-only-and-offline.md). - **Never grant a reward before** `spendCoins` resolves with `ok: true`. - **Include a `schemaVersion`** in every saved object so you can migrate later. - **Test outside the app** with the opt-in [dev mock](../guides/dev-mock.md). Prefer to let an AI do it? See [Integrate with AI](../ai-integration.mdx). --- # Coins # Coins Gamma Coins are the player's real, server-authoritative balance. The SDK never trusts the client: every spend/earn is validated and applied on the server. ## Read the balance ```js const balance = await gamma.getCoins(); // number (0 outside the app) ``` Cheap to call — read it whenever you need the current value rather than caching. ## Spend coins ```js const res = await gamma.spendCoins(50, 'extra_life'); if (res.ok) { grantExtraLife(); // only now } else { showError(res.error); // e.g. 'insufficient_coins' } ``` - `amount` — integer, `1..10000` - `reason` — lowercase `[a-z0-9_]+`, ≤ 64 chars (e.g. `extra_life`, `power_up_shield`) Result shape: ```ts { ok: boolean; newBalance?: number; spent?: number; balance?: number; error?: string } ``` > **Never grant the reward before the promise resolves with `ok: true`.** The > operation is atomic and server-authoritative — assume nothing until you get the > result. ## Earn coins Award coins for an in-game accomplishment: ```js const res = await gamma.earnCoins(5, 'checkpoint'); if (res.ok) showToast('+5 coins!'); ``` - `amount` — integer, `1..100` per call - The server enforces a hard cap of **2000 coins earned from all games per user per hour**; excess returns `{ok: false, error: 'rate_limited'}`. Result shape: ```ts { ok: boolean; newBalance?: number; earned?: number; error?: string } ``` ## Use semantic reasons The `reason` is stored in the coin audit log. Use descriptive identifiers so spend patterns can be analysed: - ✅ `extra_life`, `power_up_shield`, `skin_dragon`, `revive` - ❌ `buy`, `x`, `item1` ## Outside the app `getCoins()` returns `0` and `spendCoins`/`earnCoins` return `{ok: false, error: 'no_transport'}` unless you enable the [dev mock](./dev-mock.md). Always keep an offline path — see [App-only & offline](./app-only-and-offline.md). See also the full [API reference](../api-reference.md) and [error codes](../error-codes.md). --- # Coin value # Coin value Every integrated game shares **one canonical coin value**, so you can price in-game items and show real-world worth consistently. It's served from the Gamma Games backend and can change without you shipping a new build. ## Read it ```js const v = await gamma.getCoinValue(); // { // usdPerCoin: 0.01, // 100 coins = $1.00 // currencyCode: 'USD', // displayName: 'Gamma Coins', // symbol: 'GC', // iconUrl: null // } ``` Type: ```ts interface CoinValue { usdPerCoin: number; currencyCode: string; displayName: string; symbol: string; iconUrl: string | null; } ``` The value is cached after the first read for the session. ## Helpers ```js await gamma.coinsToUsd(100); // 1.00 — amount × usdPerCoin await gamma.formatCoins(50); // "50 GC" — uses the symbol ``` ## Pricing example ```js const {usdPerCoin, symbol} = await gamma.getCoinValue(); function priceLabel(coins) { const usd = (coins * usdPerCoin).toFixed(2); return `${coins} ${symbol} (≈ $${usd})`; } priceLabel(250); // "250 GC (≈ $2.50)" ``` ## Compatibility `getCoinValue` requires host app **GammaSDK ≥ 1.1.0**. On older hosts the call fails internally and the package returns the **default value** (`usdPerCoin: 0.01`, `USD`, `Gamma Coins`, `GC`) so your UI never breaks. Outside the app it also returns the default (it's just a display constant, not an economy operation). --- # Progress & saves # Progress & saves The SDK gives you account-bound cloud save with an automatic offline fallback. Use the built-in `gamma.storage` adapter — it handles the boilerplate (ready-waiting, fallback, and one-shot migration of an old `localStorage` save into the account). ## The adapter (recommended) ```js await gamma.storage.init(); // resolve host once const state = (await gamma.storage.load()) // account-bound in-app, localStorage outside ?? {schemaVersion: 1}; // ... play ... await gamma.storage.save(state); // returns true on success ``` - **In-app:** reads/writes the player's account, synced across devices. - **Outside the app:** transparently uses `localStorage`, so offline play works. - **First in-app launch:** if there's a legacy `localStorage` save and no account save yet, it migrates the local save into the account once, then clears it. Set the localStorage key (and other options) before the first call: ```js gamma.configure({storageKey: 'mygame_save'}); ``` ## Always include a `schemaVersion` Every persisted payload should carry an integer `schemaVersion` so future game versions can migrate old saves forward without corrupting them. ```js const CURRENT_SCHEMA = 3; function migrate(state) { if (state.schemaVersion < 2) { state.coinsEarned = 0; // field added in v2 state.schemaVersion = 2; } if (state.schemaVersion < 3) { state.levels = state.stages ?? {}; // renamed in v3 delete state.stages; state.schemaVersion = 3; } return state; } const raw = await gamma.storage.load(); const state = migrate(raw ?? {schemaVersion: CURRENT_SCHEMA}); ``` ## What to save (and what not to) Save **derived, durable** state: - ✅ Level number, per-level stars, unlock flags, cosmetic choices, counters - ✅ Quest/mission progress, tutorial completion - ✅ Settings the player explicitly changes (music on/off, control scheme) Do **not** save **reconstructible, transient** state: - ❌ Sprite positions, velocities, particle buffers, audio cursors - ❌ Camera offsets, UI animation state - ❌ Any cached coin balance — call [`getCoins`](./coins.md) when you need it ## Save cadence & limits - **Debounce** routine saves (≈1000 ms trailing); save at meaningful milestones. - **Force-save** on `visibilitychange → hidden` / `pagehide` — mobile can kill the page at any time. ```js document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') gamma.storage.save(state); }); ``` - Max encoded save size: **100 KB**. If you're close, you're probably persisting reconstructible state (see above) — compact into ids/bitfields. ## Low-level API `gamma.storage` is built on `gamma.loadProgress()` / `gamma.saveProgress(data)`, which talk to the account directly (no localStorage fallback). Prefer the adapter unless you need full control. See the [API reference](../api-reference.md). --- # Player # Player Read the player's non-sensitive public profile to personalise your game. ```js const player = await gamma.getPlayer(); if (player) { greet(player.displayName); // "Welcome back, Alex!" } ``` Type: ```ts interface Player { id: string; displayName: string; avatar: string | null; frame: string | null; isSubscribed: boolean; } ``` Returns `null` when unavailable (e.g. outside the app, or no authenticated user). > **Privacy:** `getPlayer` never returns email, phone, coin balance, or any other > sensitive field. Use it only for personalisation. For the balance, call > [`getCoins`](./coins.md). --- # App-only & offline # App-only & offline The single most important thing to understand about the SDK: > **Coins only work inside the Gamma Games app.** That's where the host injects the > real, server-authoritative bridge. Outside the app — your own website, another > portal, a plain dev browser — the SDK is **inert** by default. ## What "inert" means When the game runs outside the Gamma Games app and the [dev mock](./dev-mock.md) is not enabled: | Call | Result outside the app | |---|---| | `getCoins()` | `0` | | `spendCoins(...)` | `{ok: false, error: 'no_transport'}` | | `earnCoins(...)` | `{ok: false, error: 'no_transport'}` | | `getCoinValue()` | default value (display constant) | | `getPlayer()` | `null` | | `gamma.storage.*` | **works** — falls back to `localStorage` | Nothing can fake or spend real coins outside the app, by design. ## Always gate on `isInApp` After `gamma.ready()`, check `gamma.isInApp`: ```js await gamma.ready(); if (gamma.isInApp) { const res = await gamma.spendCoins(50, 'extra_life'); if (res.ok) grantExtraLife(); } else { // Running outside Gamma Games — use your own offline behaviour // (e.g. let the player continue for free, or hide the coin shop). } ``` `gamma.storage` keeps working everywhere, so progress/saves are unaffected — only the coin economy is app-gated. ## Why it's built this way - The coin **balance lives on the server**; the game can only *ask* to spend, and the server validates against the authenticated user. - The host supplies the user identity — the game never sends it and can't spoof it. - Outside the app there's no authenticated session, so there's nothing to spend. See [Limits & security](../limits-and-security.md) for the full model. --- # Dev mock (testing) # Dev mock The mock lets you build and test your game in a plain browser — with no Gamma Games app — by simulating the bridge locally. It is **off by default** and **never touches real balances**: state is in-memory, mirrored to `localStorage` so it survives a reload. ## Enable it Call `configure({mock: ...})` **before** `gamma.ready()`: ```js import {gamma} from '@swiftware/gamma-sdk'; gamma.configure({ readyTimeoutMs: 1500, // how long to wait for a real host first storageKey: 'mygame_save', // localStorage key for the progress fallback mock: true, // ← enable; or pass an object to seed values }); await gamma.ready(); console.log(gamma.isInApp); // always false with the mock; true only in-app ``` Seed specific values with the object form: ```js gamma.configure({ mock: { coins: 500, coinValue: {usdPerCoin: 0.02}, player: {displayName: 'Dev Player'}, }, }); ``` With the mock enabled, `getCoins`, `spendCoins`, `earnCoins`, `saveProgress`, `loadProgress`, `getPlayer`, and `getCoinValue` all work against the local simulation — including the same validation the real bridge enforces (amount/reason limits, insufficient balance, etc.). ## :warning: Don't ship it enabled In production, leave the mock **off** so the integration stays inert outside the Gamma Games app (see [App-only & offline](./app-only-and-offline.md)). A good pattern: ```js gamma.configure({mock: import.meta.env.DEV}); // only in local dev ``` ## Interactive test harness A ready-made harness that exercises every method is included with the package and mirrored here: [open the test harness](pathname:///examples/test-game.html). It shows an "in-app" vs "dev mock" badge and logs every SDK response. --- # API reference # API reference The package exposes a single `gamma` object. All methods are promise-first. ```js import {gamma} from '@swiftware/gamma-sdk'; // drop-in: global `gamma` ``` ## Lifecycle | Member | Signature | Description | |---|---|---| | `gamma.configure(config)` | `(config: GammaConfig) => void` | Set options **before** the first call (timeout, storage key, dev mock). | | `gamma.ready()` | `() => Promise` | Resolve once the SDK has settled (real host, or fallback outside the app). Idempotent. | | `gamma.isInApp` | `boolean` | `true` only when the real Gamma Games host is present. Check after `ready()`. | ```ts interface GammaConfig { readyTimeoutMs?: number; // default 1500 storageKey?: string; // default 'gamma_save' mock?: boolean | { // off by default coins?: number; player?: Partial; coinValue?: Partial; }; } ``` ## Coins | Method | Signature | |---|---| | `getCoins()` | `() => Promise` | | `spendCoins(amount, reason)` | `(number, string) => Promise` | | `earnCoins(amount, reason)` | `(number, string) => Promise` | ```ts interface SpendResult { ok: boolean; newBalance?: number; spent?: number; balance?: number; error?: GammaErrorCode } interface EarnResult { ok: boolean; newBalance?: number; earned?: number; error?: GammaErrorCode } ``` - `spendCoins` amount: `1..10000`. `earnCoins` amount: `1..100`. - `reason`: `^[a-z0-9_]+$`, ≤ 64 chars. ## Coin value | Method | Signature | |---|---| | `getCoinValue()` | `() => Promise` | | `coinsToUsd(amount)` | `(number) => Promise` | | `formatCoins(amount)` | `(number) => Promise` | ```ts interface CoinValue { usdPerCoin: number; currencyCode: string; displayName: string; symbol: string; iconUrl: string | null; } ``` ## Player | Method | Signature | |---|---| | `getPlayer()` | `() => Promise` | ```ts interface Player { id: string; displayName: string; avatar: string | null; frame: string | null; isSubscribed: boolean; } ``` ## Progress | Member | Signature | Notes | |---|---|---| | `gamma.storage.init()` | `() => Promise` | Resolve host once; returns `isInApp`. | | `gamma.storage.ready()` | `() => boolean` | Whether the host is resolved. | | `gamma.storage.load()` | `() => Promise` | Account in-app, `localStorage` outside. | | `gamma.storage.save(state)` | `(ProgressData) => Promise` | Returns `true` on success. | | `gamma.loadProgress()` | `() => Promise` | Low-level, account-only. | | `gamma.saveProgress(data)` | `(ProgressData) => Promise` | Low-level, account-only. Max 100 KB encoded. | `ProgressData` is any JSON-serialisable object — include an integer `schemaVersion`. ## Raw bridge equivalents The package wraps `window.GammaSDK` (callback-style). If you integrate manually, see [Manual integration](./manual-integration.md) for the raw method shapes and the underlying `{success, ...}` result objects. --- # Manual integration # Manual integration (raw `window.GammaSDK`) If you can't use the [`@swiftware/gamma-sdk`](./getting-started/install.md) package (no bundler, or you want zero dependencies), you can call the raw bridge that the Gamma Games app injects as `window.GammaSDK`. There's no `