---
type: reference
title: The replay contract
summary: The mandatory run/seed/verdict contract that makes a game replayable, what the run function receives and returns, how the seed binds a trace to one round, and how the server replays it.
---

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](/docs/marketplace-game-development/overview#replayable-vs-not-replayable) for that distinction.

## The `run` function

A replayable game ships a JS or WASM module exporting one function named `run`:

```ts
run(seed, config, trace) -> { passed, score, durationMs }
```

```ts
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-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 [#the-seed]

```ts
type Seed = readonly [number, number, number, number]   // 128 bits, 4 u32 words, MSW first
```

The 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

```ts
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 [#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](/docs/marketplace-game-development/engine-kit) provides ready-made deterministic primitives if you would rather not roll your own.

## How the server uses it

```mermaid
sequenceDiagram
  participant P as Player browser
  participant S as Caputchin server
  participant R as Replay isolate
  S->>S: deriveSeed(sessionId, gameId, round)
  S->>P: issue round with seed
  P->>P: play, recording the trace
  P->>S: submit trace on pass
  S->>S: re-derive the same seed
  S->>R: run(seed, config, trace)
  R-->>S: verdict { passed, score, durationMs }
  S->>S: parseVerdict, decide
```

The artifact the isolate loads is your `run` bundle, either the live `entry` or the dedicated [`run` artifact](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact) from the manifest.

## See also

- [The engine kit](/docs/marketplace-game-development/engine-kit): produce a conforming `run` from a plain reducer, with determinism handled for you.
- [The caputchin.json manifest](/docs/marketplace-game-development/the-caputchin-json-manifest#the-run-artifact): declaring the `run` artifact and its modules.
- [Build a marketplace game](/docs/marketplace-game-development/build-a-marketplace-game): recording the trace this contract replays.
- [Publish errors reference](/docs/marketplace-game-development/publish-failed-reference): the `run-not-conforming` self-check result.
