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 is published separately as @caputchin/widget; the split keeps games from transitively bundling the widget runtime — see ADR-0008. 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. Authors do not have to change anything about how they write register() — the realm difference is invisible at the SDK level.
Registration — helper form
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; 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.
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.
What is intentionally absent
- No
sessionIdparameter. The game does not see Cap-side or platform-side identifiers. The widget owns that. - No
startlistener on the bridge. The widget controls when the game starts via themodeattribute. The factory call is itself the start signal — when the SDK invokes your factory, you may begin rendering. - No widget-side
errorlistener 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
unmountmethod. 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 to0.0–1.0at the source. - No nickname / player-handle parameter. Scoreboard handles (when scoreboards ship — Post-MVP) are collected by the widget after the game completes, never by the game itself. Game authors do not see or set player identity in any form. See ADR-0014.
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. 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, 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.
Distribution
Two paths, both equivalent at runtime — see game-distribution. The SDK doesn't know or care which path delivered the script.