---
type: reference
---

# Game SDK

The contract a game author writes against. Distributed as `@caputchin/game-sdk` — a tiny package containing the `register()` helper and TypeScript types for the game contract. The customer-facing [widget](widget.md) is published separately as `@caputchin/widget`; the split keeps games from transitively bundling the widget runtime — see [ADR-0008](adr/0008-split-widget-and-game-sdk-packages.md). First-party games use the same public API — no private contract.

Games execute inside a sandboxed iframe mounted by the widget; the `register()` call writes into the iframe's own `window.Caputchin.games`, not the host page's. See [ADR-0015](adr/0015-sandbox-game-iframe.md). Authors do not have to change anything about how they write `register()` — the realm difference is invisible at the SDK level.

## Registration — helper form

```javascript
import { register } from '@caputchin/game-sdk';

register('bubble-pop', (container, bridge) => {
  // render UI into container
  bridge.complete({ score: 0.847, durationMs: 4200 });
});
```

The helper logs a console warning when an ID is already registered. Last write wins. There is no platform enforcement.

## Function signature

```
(container: HTMLElement, bridge: Bridge) => (() => void) | void

interface Bridge {
  complete(result: { score: number; durationMs?: number }): void
  error(err: { code: string; message?: string }): void
}
```

| Parameter | Notes |
|---|---|
| `container` | Element inside the game's sandboxed iframe. Render here. Style is naturally scoped — the iframe is its own document. |
| `bridge` | Push-only interface to the widget. The game **emits** events upward; it does not subscribe. The widget controls game start via [`mode`](widget.md#modes); the game does not need a start signal. Nickname collection is widget-owned. Widget-side errors are not surfaced to the game. |
| Return | Optional cleanup function. Called by the widget if the game is unmounted. |

`score` is normalized `0.0–1.0`. There is no `maxScore`. The score is gameplay metadata, not a security signal — see [principles](principles.md#game-data-is-metadata-not-security).

## Bridge methods

| Method | Effect |
|---|---|
| `bridge.complete({ score, durationMs })` | Round finished. May fire multiple times (replay, multi-round). The widget releases the held Cap `redeem` on the first call and locks the wrapped token; subsequent calls record additional rounds for scoreboard purposes only. `durationMs` is optional. |
| `bridge.error({ code, message })` | The game failed in a way the widget should surface. Triggers the widget's `error` event to the customer. The game is expected to release resources via its returned cleanup function. |

The bridge is push-only **at MVP**. Game-load customization (theme, language, container size hints) will arrive as a third parameter at factory invocation in a later release — not as an event. See [roadmap](roadmap.md#paid-tier-ideas--deferred-explore-later).

## What is intentionally absent

- **No `sessionId` parameter.** The game does not see Cap-side or platform-side identifiers. The widget owns that.
- **No `start` listener on the bridge.** The widget controls when the game starts via the [`mode` attribute](widget.md#modes). The factory call is itself the start signal — when the SDK invokes your factory, you may begin rendering.
- **No widget-side `error` listener on the bridge.** Errors in the widget realm are not surfaced into the iframe. The game does not need to react; its own cleanup function handles unmount.
- **No `unmount` method.** Return a cleanup function from your render call instead — closes over the right state without forcing a method shape.
- **No lifecycle hooks** (mount, pause, resume). Render once; clean up via the returned function.
- **No `maxScore`.** Normalize to `0.0–1.0` at the source.
- **No nickname / player-handle parameter.** Scoreboard handles (when scoreboards ship — Post-MVP) are collected by the [widget](widget.md) after the game completes, never by the game itself. Game authors do not see or set player identity in any form. See [ADR-0014](adr/0014-scoreboard-3letter-async-naming.md).

The contract is intentionally tiny so it can be stable across many years of game catalogue growth.

## Multi-round and replay

`bridge.complete(...)` may fire multiple times. Common patterns:

- "Play again" button after a round
- Multi-round games where each round emits a score

The platform handles dedup of verification: the first `bridge.complete(...)` redeems Cap and locks the wrapped token; subsequent calls feed scoreboard data only. See [api](api.md#post-gamecomplete-browser--platform). Game authors do not need to think about this — call `bridge.complete(...)` whenever a round finishes, in whatever the natural cadence is for the game.

## Defaults expected of marketplace games

These are author-declared in the [marketplace manifest](marketplace.md#support-flags), never enforced. First-party games should ship `responsive: true`, `touch: true`, and `accessible: true` to set the bar.

## Bundle requirements

The iframe loads exactly one script URL per game. Everything the game needs at runtime must be inside that bundle: sprites, sounds, fonts, WASM. Configure your bundler for a single self-contained output with assets inlined as data URLs and no code splitting. Path-relative `fetch`, dynamic `import()`, URL-spawned `Worker`, and external CDN fetches do not work inside the sandbox. See [game-distribution](game-distribution.md#bundle-constraint).

## Distribution

Two paths, both equivalent at runtime — see [game-distribution](game-distribution.md). The SDK doesn't know or care which path delivered the script.
