The replay contract
The replay contract is the one mandatory surface a replayable game agrees on with the platform. It is published as @caputchin/replay-contract: a deliberately tiny, dependency-free package (it is loaded into a sealed replay isolate, so it pulls in nothing). Everything else about your game, the engine, the renderer, the trace format, is yours.
A game that ships a conforming run is replayable and so can gate a site key; a game without one is still embeddable, but only as UX (it shows Not replayable). See Ship a game to the marketplace for that distinction.
The run function
A replayable game ships a JS or WASM module exporting one function named run:
run(seed, config, trace) -> { passed, score, durationMs }type RunFn<C = unknown> = (
seed: Seed,
config: C | null,
trace: Uint8Array | string,
) => Verdict | Promise<Verdict>| Argument | Source | Meaning |
|---|---|---|
seed | Server | The per-round seed. The round's server-owned setup. |
config | Server | The gameplay config, nullable and opaque: the platform never inspects it, and it is re-resolved server-side at replay, never client-asserted. null means "use the run's own defaults". At MVP the server passes null; per-site config is a deferred phase. |
trace | Client | The opaque blob the player's client emitted, which this function alone interprets. The platform never parses or types it; it is raw bytes or a string, bounded only by a size cap and the isolate CPU limit. |
The argument order encodes the trust model: seed and config are the round's server setup; trace is the player's input. Gate-affecting values (a pass threshold, lives) are safe to read from config but would be a bypass if read from the client trace.
The module must export the function under the name run (the constant RUN_EXPORT_NAME). The replay host invokes it and awaits the result (WASM instantiation is async on first call).
The seed
type Seed = readonly [number, number, number, number] // 128 bits, 4 u32 words, MSW firstThe seed is the low 128 bits of SHA-256(sessionId : gameId : roundIndex), carried as four unsigned 32-bit words, most-significant word first. The package's deriveSeed(sessionId, gameId, roundIndex) computes it; the server derives it both when issuing the round and again when replaying, so it never rides the wire as trusted client input.
Crucially, the seed binds a trace to one session, game, and round: replaying a foreign or earlier trace under a different seed yields passed: false. That binding is how trace injection and replay attacks are defended, so you must seed all randomness from it for the live play and the server replay to agree.
The verdict
interface Verdict {
readonly passed: boolean // drives the verification decision
readonly score: number // game-defined, any finite number
readonly durationMs: number // finite, non-negative
}passed is the only field that drives the captcha decision; score and durationMs are carried in the issued token (and a future scoreboard). The package's parseVerdict(value) validates an untrusted return value and treats a malformed result as a rejected round, never a passing one, so a run that throws or returns garbage fails closed.
Determinism is the whole requirement
The contract's one hard rule: run must be pure and deterministic across two runtimes, the player's browser and the server isolate. Identical (seed, config, trace) must yield an identical verdict in both. The common ways to break this, all caught by the publish self-check as run-not-conforming:
- reading
Date.now(),performance.now(),Math.random(), or other non-deterministic globals; - reading external state (DOM, network, storage) the replay isolate does not provide;
- relying on floating-point math that differs between runtimes.
How you achieve determinism (fixed-point, WASM-spec floats, or IEEE-754 plus a neutralization shim) is your choice; the platform only hosts the replay. The engine kit provides ready-made deterministic primitives if you would rather not roll your own.
How the server uses it
The artifact the isolate loads is your run bundle, either the live entry or the dedicated run artifact from the manifest.
See also
- The engine kit: produce a conforming
runfrom a plain reducer, with determinism handled for you. - The caputchin.json manifest: declaring the
runartifact and its modules. - Build a marketplace game: recording the trace this contract replays.
- Publish errors reference: the
run-not-conformingself-check result.