# 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 `