Skip to content

Chain.wtf — building casino games guide

Wire your casino game's contracts, iframe frontend, manifest, and host bridge together. Follow the session lifecycle from wager to outcome reveal.

Jump to:

  1. Contract
  2. Guest lifecycle
  3. Snapshot
  4. Manifest
  5. Frontend patterns

Chain.wtf is a fully decentralized wagering platform that consists of Sportsbook, Casino, fully on chain PvP games and predcition markets. It’s built on top of Coinbase’s Base L2 network and a unified liquidity pool that is used to back the winnings of the players.

This document is self-contained: you can follow it from a separate repository (e.g. Vite frontend + Forge/Hardhat) without any access to internal Chain.wtf source trees. Inline code blocks are the canonical types and patterns you need. To start from a working game, follow Getting Started.


Host (Chain.wtf web application) owns the user’s wallet, Smart Vault, session keys, and all transaction signing. It loads your game in an iframe and talks to it over a promise-based bridge (Penpal over postMessage). The iframe is typically sandboxed with allow-scripts allow-same-origin so the child origin is stable and the bridge can connect.

Guest (your game UI): does not connect a wallet or send raw transactions. It only:

  • Encodes user intent as ABI-encoded gameData (at session open) and actionData (per player step).
  • Calls hostApi.openSession, hostApi.submitAction, and optionally hostApi.cancelStuckRandomness — each returns a Promise (host signs/broadcasts txs).
  • Calls hostApi.revealOutcome once its result presentation has finished, so the host credits withheld winnings to its balance displays (see §3.4).
  • Renders from HostSnapshot, pushed by the host via guestApi.setState(snapshot) whenever wallet, balances, sessions, or UI settings change.

Chain: the protocol exposes CasinoGameFacet on the diamond/proxy. Your deployed game contract implements ICasinoGameV2. The facet orchestrates escrow, vault liquidity caps, randomness requests, and calls into your game’s pure/view step handlers.


Your game contract must implement this interface. Phases, context, and step results are fixed by the platform.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
enum SessionPhase {
NONE,
WAITING_RANDOMNESS,
WAITING_PLAYER_ACTION,
SETTLED,
FORFEITED,
CANCELLED
}
struct SessionContext {
uint256 sessionId;
address player;
address vault;
uint256 wagerBase;
uint256 escrowedStake;
uint256 reservedProfit;
uint32 step;
bytes gameData;
bytes gameState;
}
struct StepResult {
bytes newGameState;
int256 escrowDelta;
int256 reservedProfitDelta;
SessionPhase nextPhase;
bool requestRandomnessNow;
uint256 payout;
}
interface ICasinoGameV2 {
function quoteCaps(
uint256 wager,
bytes calldata gameData
) external view returns (uint256 maxEscrowStake, uint256 maxReservedProfit);
function quoteRiskParams(
uint256 wager,
bytes calldata gameData
)
external
view
returns (
uint256 maxPayout,
uint256 probabilityWad,
uint256 expectedPayout,
uint256 bodyVarianceScaled
);
function onSessionStart(
SessionContext calldata ctx
) external view returns (StepResult memory);
function onPlayerAction(
SessionContext calldata ctx,
bytes calldata actionData
) external view returns (StepResult memory);
function onRandomness(
SessionContext calldata ctx,
bytes32 randomness
) external view returns (StepResult memory);
function quoteForfeitPayout(SessionContext calldata ctx) external view returns (uint256 cashoutValue);
}

Semantics:

  • quoteCaps: Before openSession, the facet requires maxEscrowStake >= wager and holds maxReservedProfit, the session’s largest net win, to the single-win cap.
  • quoteRiskParams: Supplies the four figures the facet prices the session on: its fees, its bet-size cap and its share of the vault reserve (§2.1.1). expectedPayout is RTP × wager, the mean total payout, under the player’s best policy when the game has player choices. maxPayout is the largest total player payout. probabilityWad is the probability of exactly maxPayout in WAD (1e18 = 100%), not the chance of any win. bodyVarianceScaled is the body variance: the variance of the round’s payout with the top tier excluded, per bet, in wei² × 1e18. It is counted for every bet; only a game whose sole winning outcome is the top tier returns 0. See SLOTS_RISK_AND_RESERVES.md.
  • onSessionStart: Called immediately after the session is created with the player’s wager pulled in; you return the next phase and updated opaque newGameState.
  • onPlayerAction: Only when the facet is in WAITING_PLAYER_ACTION and the player calls submitAction in time (by the session’s deadline, a block timestamp).
  • onRandomness: Called when the randomness provider fulfills a request; your game consumes bytes32 RNG and returns the next step or terminal settlement. That step waits for the player or settles; it may not request randomness again. See One random value per request.
  • quoteForfeitPayout: The current cash-out value (stake + accrued winnings) derived from ctx.gameState; return 0 when nothing is cashable mid-round. When an abandoned session is forfeited after its deadline, the facet pays the player this quote (clamped to escrowedStake + reservedProfit) minus a 10% cut instead of confiscating the whole stake. The call is defensive: if it reverts or returns malformed data — or the game predates the method — the forfeit pays 0. Implement it faithfully only in games with a true anytime cash-out whose value is fully determined by already-revealed state (mines-style). Games whose mid-round value depends on unresolved randomness or hidden state (blackjack-style) must return 0: any concrete quote there is an adverse-selection exploit — the player abandons exactly when the real continuation value is below the quote (a bad blackjack hand can be worth ~0.3× stake while a stake-based quote pays out 0.9×), turning forfeiture into a free put option against the vault.

A game gets one bytes32 per randomness request. When a step needs several random values (two dice, a hand of cards, a mine layout), derive them all from that one value, for example keccak256(abi.encode(randomness, i)) for the i-th value, or the byte cursor in RANDOMNESS_DICE.md.

Randomness is only requested from a player’s call (onSessionStart or onPlayerAction), because the call that requests it pays its fee. If onRandomness returns WAITING_RANDOMNESS, the facet reverts the callback with CasinoGameFacet__RandomnessRequestedFromCallback: the delivery fails and the result is never applied. Once the randomness deadline passes, anyone can call cancelStuckRandomness, which refunds the session’s whole remaining escrow and takes no casino fee.

Games that map facet bytes32 randomness to six-sided dice must use rejection sampling — accept only bytes < 252, then (byte % 6) + 1. Do not use raw randomness[i] % 6 on a byte (modulo bias: faces 1–4 are slightly more likely than 5–6).

Full algorithm, Solidity/TypeScript reference implementations, and an agent DO/DON’T checklist: RANDOMNESS_DICE.md.

Escrow / profit:

  • escrowDelta: positive = extra tokens pulled from the player into facet escrow (e.g. double down); negative = release to player. Only onSessionStart and onPlayerAction may change the escrow. If onRandomness returns a nonzero escrowDelta, the facet reverts the callback with CasinoGameFacet__EscrowChangedFromCallback, and the session can only be cancelled with cancelStuckRandomness.
  • reservedProfitDelta: commits vault “reserved profit” (usually the full maxReservedProfit in onSessionStart). The win is capped at escrowedStake + reservedProfit, so return 0 on the settling step. The facet releases the reserve at settlement, and releasing it yourself reverts every win above 1x. See CONTRACT_CONSTRAINTS.md § Payout cap.

Your two quote functions are read once, at openSession. They decide the session’s fees, the largest wager the vault accepts and the reserve the session needs:

  • quoteCaps answers: “What is the largest escrow / reserved-profit this single session may need?”
  • quoteRiskParams answers: “What does this session pay on average, what is the most it pays, how likely is that, and how much does the rest vary?”

The facet uses both during openSession, in this order:

  1. Require maxEscrowStake >= wager.
  2. Check the quote against the guardrails: RTP inside the council-set RTP band (60% to 99.9% by default), maxPayout at least expectedPayout, a body variance above zero when outcomes below maxPayout pay, and a positive plain variance. A failing quote reverts with QuoteFiguresLib__RtpOutsideBand, QuoteFiguresLib__MaxPayoutBelowExpectedPayout, QuoteFiguresLib__BodyVarianceMissing or QuoteFiguresLib__VarianceNotPositive.
  3. Set the session’s fees from its edge, wager − expectedPayout.
  4. Check the bet-size cap: the single-win cap on maxReservedProfit, and the risk budget and the break-even rule on the wager.
  5. Pin the session’s per-bet variance — quoted body variance plus the top-tier binary term from (maxPayout, probabilityWad) — add it into the vault aggregate and recompute the required casino reserve.
  6. Revert if the reserve does not fit in the vault’s available liquidity.

Fees. The edge is split in order: your game dev fee first (10% of the edge by default), then the LP cut and the protocol fee from the net edge that is left, and the operator fee is the remainder. A $100 wager at 96% RTP has an edge of $4.00, so the default cut pays you $0.40. Fees are paid when the session settles or is forfeited, whether the player won or lost. A cancelled session pays no fee. The full split with a worked example, and how to read and claim your fees, is in CONTRACT_CONSTRAINTS.md § Fee split.

Largest wager. The largest wager the vault accepts falls as your game’s variance rises and as its edge falls. Never compute it in the frontend: ask the host (§4, Asking the host for the maximum wager).

What each figure must mean, the formulas and every error are in CONTRACT_CONSTRAINTS.md § What your quotes decide.

For the current built-in games:

  • Coinflip returns the exact combinatorial win probability in WAD.
  • Mines returns the probability of the full-board cashout path 1 / C(25, mineCount) in WAD.
  • Blackjack returns the probability of its 5x round under the best policy: split, double both hands, win both. Its maxPayout of 5x is net of the stake the split and the doubles add after opening.
  • Slots and any multi-tier paytable must return the body variance in bodyVarianceScaled; a quote with several winning tiers and a body variance of 0 is refused at whitelisting and at openSession.

2.2 CasinoGameFacet responsibilities (summary)

Section titled “2.2 CasinoGameFacet responsibilities (summary)”

The facet (names only — your deployment uses the protocol’s verified ABI):

EntryRole
openSession(game, vault, wager, gameData)Validates whitelist, vault, caps from quoteCaps and the quote from quoteRiskParams (guardrails, bet-size cap), sets the session’s fees, updates the casino portfolio reserve, transfers wager in, creates session, runs onSessionStart, then applies StepResult (may request RNG, wait for player, or finalize). An overload takes a trailing operator address (§7.1).
submitAction(encodedSession, actionData)Requires phase WAITING_PLAYER_ACTION, player msg.sender, not past action deadline; runs onPlayerAction. encodedSession is the latest committed snapshot (see below).
onCasinoRandomnessFulfilled(requestId, randomness, clientData)Randomness provider callback; clientData is the committed snapshot the facet handed to the provider; runs onRandomness on the game.
CasinoConfigFacet.getSessionCommitment(sessionId) / decodeSession(bytes)Sessions are not stored on chain — only keccak256(encodedSession). Every non-terminal step emits CasinoSessionAdvanced(sessionId, step, requestId, randomness, session) with the full encoded snapshot (gameData, gameState, phase, deadline, stake, caps…), and every later call echoes the latest one back.
forfeitExpiredSession(bytes) / forfeitExpiredSessions(bytes[])If phase WAITING_PLAYER_ACTION and block > action deadline → forfeit.
cancelStuckRandomness(bytes)If phase WAITING_RANDOMNESS and randomness deadline passed → cancel and refund per facet rules.

Randomness is requested by the facet from its configured IRandomnessProvider (in production a Verify Network adapter dedicated to the casino vertical). openSession and submitAction are payable: a call whose step requests randomness passes all the ETH it carries to the provider as the request’s fee, and a call whose step requests none gets its ETH back in the same transaction. Your game contract never sees this ETH. The bridge’s randomnessRequestData field is deprecated and ignored by the host. The game iframe never receives the RNG directly from a wallet; settlement is on-chain.

2.3 Contract patterns: instant vs multi-action

Section titled “2.3 Contract patterns: instant vs multi-action”

Instant game (e.g. coinflip) — session resolves without submitAction:

  • onSessionStart → nextPhase = WAITING_RANDOMNESS, requestRandomnessNow = true.
  • onPlayerAction reverts (no player steps).
  • onRandomness → SETTLED with payout (encode the result in newGameState).

Illustrative excerpt:

function onSessionStart(
SessionContext calldata ctx
) external pure returns (StepResult memory stepResult) {
// ... decode ctx.gameData, compute maxReservedProfit ...
stepResult.newGameState = abi.encode(/* opaque state */);
stepResult.escrowDelta = 0;
stepResult.reservedProfitDelta = int256(maxReservedProfit);
stepResult.nextPhase = SessionPhase.WAITING_RANDOMNESS;
stepResult.requestRandomnessNow = true;
stepResult.payout = 0;
}
function onPlayerAction(
SessionContext calldata,
bytes calldata
) external pure returns (StepResult memory) {
revert CoinflipGame__NoPlayerAction();
}
function onRandomness(
SessionContext calldata ctx,
bytes32 randomness
) external pure returns (StepResult memory stepResult) {
// ... compute payout, set stepResult.nextPhase = SessionPhase.SETTLED, etc.
}

Multi-action game (e.g. mines) — opens on player’s turn:

  • onSessionStart → WAITING_PLAYER_ACTION, requestRandomnessNow = false.
  • onPlayerAction decodes actionData (reveal tile, cash out, etc.); may transition to WAITING_RANDOMNESS with requestRandomnessNow = true for a single tile reveal, then onRandomness returns to WAITING_PLAYER_ACTION or settles.

Illustrative excerpt:

function onSessionStart(
SessionContext calldata ctx
) external pure returns (StepResult memory stepResult) {
uint8 mineCount = _decodeAndValidateGameData(ctx.gameData);
uint256 maxPayout = _cashoutPayout(ctx.wagerBase, mineCount, TILE_COUNT - mineCount);
uint256 maxReservedProfit = maxPayout > ctx.wagerBase ? maxPayout - ctx.wagerBase : 0;
stepResult.newGameState = abi.encode(/* initial MinesState */);
stepResult.escrowDelta = 0;
stepResult.reservedProfitDelta = int256(maxReservedProfit);
stepResult.nextPhase = SessionPhase.WAITING_PLAYER_ACTION;
stepResult.requestRandomnessNow = false;
stepResult.payout = 0;
}

2.4 Sequence: open session → chain → snapshot back to iframe

Section titled “2.4 Sequence: open session → chain → snapshot back to iframe”
SDK flow diagramloop[Player_actions]openSession(wager, gameData)openSession(game, vault, wager, gameData)onSessionStart(ctx)StepResulttx receipt / logssetState(HostSnapshot)submitAction(sessionId, actionData, ...)submitActiononPlayerAction(ctx, actionData)StepResultsetState(...)onRandomness(ctx, randomness)setState(...)Randomness provider fulfills RNG off diagramGame_iframeHost_appCasinoGameFacetICasinoGameV2Game_iframeHost_appCasinoGameFacetICasinoGameV2

Install @chain-protocol/casino-sdk from npm with npm install @chain-protocol/casino-sdk or bun add @chain-protocol/casino-sdk. A game imports @chain-protocol/casino-sdk (types, manifest validation, hostSupports) and @chain-protocol/casino-sdk/guest (connectGameToHost). The listings below are bridge source excerpts; omitted helpers and additional exports remain in src/types.ts, src/guest.ts, and src/host.ts.

./host, ./simulator and ./conformance are internal subpaths. They serve the Chain.wtf host, chain dev casino and game-verify, sit outside semver and can change in any release. §3.3 shows the host side so you know what the parent page does; do not import it from a game.

Dependency: the SDK depends on penpal ^7.0.4 (the child connects to window.parent; the host connects to iframe.contentWindow).

export type HexString = `0x${string}`;
export type CasinoGameManifest = {
schemaVersion: 1;
gameId: string;
defaultLocale: string;
locales: Record<
string,
{
name: string;
description?: string;
}
>;
assets?: {
iconUrl?: string;
coverUrl?: string;
};
};
export type HostSnapshot = {
/** Version of the SDK the host was built with; branch on it with `hostSupports`. */
sdkVersion: string;
integration: {
chainId: number;
slug: string;
gameAddress: `0x${string}`;
manifest: CasinoGameManifest;
};
wallet: {
address?: `0x${string}`;
smartVaultAddress?: `0x${string}`;
status: 'ready' | 'disconnected' | 'setup-required' | 'session-key-mismatch';
};
token: {
symbol?: string;
decimals?: number;
};
balances: {
smartVaultBalance?: string;
};
sessions: {
items: Array<{
sessionId: string;
sessionKey: string;
gameAddress: `0x${string}`;
// Compare against the exported `SessionPhase`, e.g. `phase === SessionPhase.SETTLED`.
phase?: number;
phaseName?: SessionPhaseName; // 'NONE' | 'WAITING_RANDOMNESS' | … | 'CANCELLED'
wager?: string;
// Total escrowed stake — wager plus mid-session increases (e.g. a
// blackjack double), from the committed session snapshot. Absent on
// older hosts; fall back to `wager`.
stake?: string;
payout?: string;
isSettled: boolean;
openedAt?: number;
settledAt?: number;
lastEventTimestamp: number;
raw: {
gameData?: HexString;
gameState?: HexString;
randomness?: HexString;
requestId?: HexString;
};
}>;
};
ui: {
locale: string;
theme: 'light' | 'dark' | 'system';
viewport?: {
// Height in px from the top of the game's iframe to the bottom edge of
// the visible screen, before the user scrolls. To place your primary
// action (e.g. the bet button) exactly at the screen edge, size the
// content above it to `availableHeight` minus the action's own height.
// Do not use CSS viewport units for this: the host grows the iframe to
// fit your content, so inside the iframe 100vh equals the full content
// height, not the visible screen.
availableHeight: number;
};
};
};
export type MaxWagerResult =
| { kind: 'limit'; maxWager: string }
| { kind: 'no-limit' }
| { kind: 'unknown' };
export type HostCapabilities = { playAccess?: boolean };
export type HostApi = {
/** Optional UI request; acknowledge presentation, never readiness. Discover playAccess first. */
requestPlayAccess?(): Promise<void>;
reportContentSize?(input: { minHeight: number }): Promise<void>;
getMaxWager(input?: { gameData?: HexString }): Promise<MaxWagerResult>;
openSession(input: {
wager: string;
gameData: HexString;
/** @deprecated Ignored: the casino facet requests randomness itself. */
randomnessRequestData?: HexString;
}): Promise<{ sessionKey: string; transactionHash: HexString }>;
submitAction(input: {
sessionId: string;
actionData: HexString;
/** @deprecated Ignored: the casino facet requests randomness itself. */
randomnessRequestData?: HexString;
approvalAmount?: string;
}): Promise<{ transactionHash: HexString }>;
cancelStuckRandomness(input: { sessionId: string }): Promise<{ transactionHash: HexString }>;
revealOutcome(input: { sessionId: string }): Promise<void>;
};
export type GuestPanelName = 'info' | 'history' | 'audio';
export type GuestCapabilities = {
panels?: GuestPanelName[];
};
export type GuestConnectOptions = {
capabilities?: GuestCapabilities;
};
export type GuestApi = {
/** Show or focus an advertised intent idempotently; resolve when accepted, not dismissed. Reject unsupported requests. */
openPanel?(input: { panel: GuestPanelName }): Promise<void>;
setState(snapshot: HostSnapshot | null): Promise<void>;
};
export type CasinoGameManifestValidationResult =
| {
ok: true;
manifest: CasinoGameManifest;
}
| {
ok: false;
reason: string;
};
export type GameManifestLocale = {
locale: string;
name: string;
description?: string;
};
export type GameManifestMetadata = {
locale: string;
name: string;
description?: string;
iconUrl?: string;
coverUrl?: string;
};
import { WindowMessenger, connect } from 'penpal';
import type { Connection } from 'penpal';
import { addHostCapabilities, createGuestMethods } from './capabilities';
import type { HostRpcApi } from './capabilities';
import type { GuestApi, GuestConnectOptions, HostApi, HostCapabilities } from './types';
export type {
GuestPanelName,
GuestApi,
GuestCapabilities,
GuestConnectOptions,
HostApi,
HostCapabilities,
HostSnapshot,
MaxWagerResult,
SessionPhaseName,
} from './types';
export { SessionPhase } from './types';
/**
* Peer capabilities are discovered independently of snapshots and ordinary RPC.
* Penpal fabricates callable proxies for missing methods: await capabilities and
* require playAccess === true before using the access intent. Proxy presence is
* never evidence of support. SDK destruction rejects pending discovery/calls.
*/
export type GuestBridgeConnection = Connection<HostRpcApi> & {
capabilities: Promise<HostCapabilities>;
};
export type ContentSizeObserver = {
disconnect(): void;
report(): void;
};
const getAllowedParentOrigins = (): string[] => {
if (typeof document === 'undefined' || !document.referrer) {
return ['*'];
}
try {
return [new URL(document.referrer).origin];
} catch {
return ['*'];
}
};
export const connectGameToHost = (
methods: GuestApi,
options?: GuestConnectOptions,
): GuestBridgeConnection => {
const guestMethods = createGuestMethods(methods, options?.capabilities);
return addHostCapabilities(
connect<HostRpcApi>({
messenger: new WindowMessenger({
remoteWindow: window.parent,
allowedOrigins: getAllowedParentOrigins(),
}),
methods: guestMethods,
}),
);
};

createGuestMethods (src/capabilities.ts) validates the optional declaration and supplies SDK-owned getCapabilities; games implement behavior, never discovery. connectGameToHost(guestMethods) remains valid for guests without panels. To advertise a guest-owned History panel, supply an openPanel handler and the optional second argument:

const connection = connectGameToHost(
{
setState: guestMethods.setState.bind(guestMethods),
async openPanel({ panel }) {
if (panel !== 'history') throw new Error('Unsupported panel.');
await showOrFocusHistory(); // Game-owned UI handler; repeated calls preserve panel state.
},
},
{ capabilities: { panels: ['history'] } },
);

presentation.hostPanels.history in the manifest requests host-drawn chrome; connection panels: ['history'] advertises a guest-owned panel, and does not add panels to the manifest. Declare only implemented intents; Casino supports info, history, and audio (game audio controls, which may focus an existing Settings surface).

import { WindowMessenger, connect } from 'penpal';
import type { Connection } from 'penpal';
import type { GuestCapabilities, HostApi, HostCapabilities } from './types';
import { addCapabilities, createHostMethods } from './capabilities';
import type { GuestRpcApi } from './capabilities';
import { version } from '../package.json';
export const SDK_VERSION: string = version;
export type {
GuestApi,
GuestCapabilities,
GuestPanelName,
HostApi,
HostCapabilities,
HostSnapshot,
} from './types';
// Penpal fabricates callable proxies even for missing methods. Discovery, not
// property presence, determines support; missing RPCs reject METHOD_NOT_FOUND.
export type HostBridgeConnection = Connection<GuestRpcApi> & {
capabilities: Promise<GuestCapabilities>;
};
export const connectHostToGame = (options: {
iframe: HTMLIFrameElement;
childOrigin: string;
methods: HostApi;
capabilities?: HostCapabilities;
}): HostBridgeConnection => {
const remoteWindow = options.iframe.contentWindow;
if (!remoteWindow) {
throw new Error('Iframe contentWindow is not available.');
}
const hostMethods = createHostMethods(options.methods, options.capabilities);
return addCapabilities(
connect<GuestRpcApi>({
messenger: new WindowMessenger({
remoteWindow,
allowedOrigins: [options.childOrigin],
}),
methods: hostMethods.methods,
}),
hostMethods.destroy,
);
};

The imported GuestRpcApi and addCapabilities (src/capabilities.ts) keep discovery inside the SDK. GuestRpcApi uses Required<GuestApi> plus SDK-owned getCapabilities, so Penpal’s remote proxy retains callable openPanel signatures and per-call CallOptions; this does not make openPanel required in guest implementations. Hosts use connection.capabilities to determine support: Penpal can fabricate a callable proxy for a missing method, so typeof guestApi.openPanel is not feature detection. A legacy guest without the discovery RPC yields {}; other discovery failures reject. Keep controls unavailable when discovery or a panel request fails, and destroy the connection on teardown.

The guest connection also discovers Host capabilities. The optional playAccess intent requests existing Host access UI; it does not authenticate, sign or place a bet. See Requesting play access for declaration, lifecycle and unsupported-peer behavior. Guest panels and Host access are independent directions on the same bridge.

A game may ask the Host to present its existing login, vault setup or signing-key recovery UI. It grants no account access or transaction authority. On the connection returned by connectGameToHost, read await connection.capabilities; offer the new action only when playAccess === true. Missing discovery on an older Host resolves to {}; other discovery failures disable this affordance without blocking normal snapshots or gameplay RPCs.

const capabilities = await connection.capabilities;
const hostApi = await connection.promise;
// Call only from an explicit player action, after checking the game's access gates.
async function requestAccessFromClick() {
if (capabilities.playAccess !== true) return;
await hostApi.requestPlayAccess();
// Acknowledges UI presentation, not login success. Wait for authoritative snapshots.
}

The Host opts in with connectHostToGame({ iframe, childOrigin, methods, capabilities: { playAccess: true } }), implementing HostApi.requestPlayAccess. The SDK owns getCapabilities; do not implement that reserved RPC. An affirmative declaration without a callable implementation throws at connection creation. Capability discovery is per connection, not a readiness or permission signal.

The request takes no provider, account, route or transaction arguments and resolves once the current Host flow has opened/focused, or when the account is already ready. The Host must check its current player/game/deployment context, preserve unrelated modal and recovery ownership, reject an unavailable flow and coalesce or focus repeated UI requests. Concurrent pending requests share one invocation; destroying the connection rejects pending requests and stops queued ones. It cannot undo UI already opened or cancel an operation the player has separately authorized. Hosts must fence their own asynchronous continuations against context changes.

Keep betting disabled until a new Host snapshot permits it, and require a fresh bet action. Rejections and timeouts never imply readiness or prove that remote UI did not open; do not automatically retry. Use localized fallback guidance rather than parsing error text. The SDK adds no stable policy-error taxonomy, wallet kit, signing call, funding request or automatic bet. Existing Host entry points remain available on unsupported peers.

Casino exposes the new RPC only with playAccess: true; supplying the method alone does not enable it. This is separate from guest panel declarations and from game manifests.

  1. On mount, call connectGameToHost with GuestApi where setState stores the latest snapshot in React state (or your framework); guests with panels also implement optional openPanel and declare those intents in the optional second argument. The SDK owns getCapabilities for that connection.
  2. await connection.promise (or .then) to receive HostApi — enable buttons that call openSession / submitAction only after this resolves.
  3. When your result presentation finishes (reels stopped, bonus round played out), you must call hostApi.revealOutcome({ sessionId }) so the host credits the payout to its balance UI. Sessions settle on-chain well before your animation ends; from bet placement until your reveal, the host clamps its balance displays so they can move down but never up — debits show, winnings stay hidden — so the top-bar balance never spoils the outcome. It is the reverse of an optimistic update: the payout is final, only its display is delayed until you reveal it. The guard only exists while your game is mounted: refreshing the page or navigating away shows the real balance immediately. Losses need no reveal (the wager left at open, so settlement moves no balance and the host releases the guard itself), but calling it after a loss is a harmless no-op.
  4. On unmount, call connection.destroy().

The host must:

  1. Create an iframe with src pointing at your deployed game URL (HTTPS origin = childOrigin).
  2. Call connectHostToGame({ iframe, childOrigin, methods }) where methods implements HostApi (real tx signing).
  3. When connection.promise resolves, call guestApi.setState(snapshot) once with the current snapshot.
  4. On every snapshot update (wallet, sessions, balances), call guestApi.setState again.
  5. Independently await connection.capabilities before enabling guest-panel controls; initial setState does not wait for discovery. Dispatch only advertised intents through guestApi.openPanel in response to the user’s action.

Use stable function references for getMaxWager / openSession / submitAction / cancelStuckRandomness / revealOutcome that delegate to the latest closure (so reconnects do not stale). Minimal illustration:

const latestMethodsRef = useRef(hostMethods);
latestMethodsRef.current = hostMethods;
const stableMethods: HostApi = {
getMaxWager: input => latestMethodsRef.current.getMaxWager(input),
openSession: input => latestMethodsRef.current.openSession(input),
submitAction: input => latestMethodsRef.current.submitAction(input),
cancelStuckRandomness: input => latestMethodsRef.current.cancelStuckRandomness(input),
revealOutcome: input => latestMethodsRef.current.revealOutcome(input),
};
const connection = connectHostToGame({
iframe,
childOrigin: new URL(gameUrl).origin,
methods: stableMethods,
});
void connection.promise.then(async guestApi => {
await guestApi.setState(currentSnapshot);
});
// Call from the host's History control; handle rejection in the control's error UI.
async function openGuestHistory() {
const capabilities = await connection.capabilities;
if (!capabilities.panels?.includes('history')) return;
const guestApi = await connection.promise;
await guestApi.openPanel({ panel: 'history' });
}

Teardown: destroy the Penpal connection when the iframe is removed. Avoid setState(null) racing iframe teardown (can throw if the connection is already destroyed).


FieldUse
wallet.statusOnly enable betting when 'ready'; show setup prompts for setup-required / session-key-mismatch.
token.decimalsParse human wager input with viem parseUnits.
token.iconUrlOptional absolute URL of the token icon for balance / wager displays.
balances.smartVaultBalanceDisplay spendable balance (host-defined).
sessions.itemsEach row: sessionId, sessionKey, phaseName, wager, payout, raw.gameData, raw.gameState.

sessionKey format (host convention): ${chainId}:${sessionIdDecimal} — use it to match a session after openSession returns until the indexer + on-chain view catch up.

Terminal phases: SETTLED, FORFEITED, CANCELLED — treat as ended for UX.

snapshot.sdkVersion is the version of the SDK the host was built with, for example 0.6.0 or 0.7.0-canary.1. Hosts older than 0.5.0 leave it out. Branch on it with hostSupports, never by parsing the string:

import { hostSupports } from '@chain-protocol/casino-sdk';
if (hostSupports(snapshot, '0.6.0')) {
// the 0.6.0 behavior
} else {
// the behavior of older hosts
}

hostSupports compares major, minor and patch and ignores the prerelease tag, so a canary host answers as the version it previews. It answers false when sdkVersion is absent.

The casino SDK, the PvP SDK and @chain-protocol/cli publish together at one version. In 0.x, a minor bump such as 0.5.x to 0.6.0 is breaking and a patch is not, so a ^0.5.0 range takes patches and never the next breaking release. Install the SDK and the CLI at the same version. chain init writes both as a ^ range of the CLI version that created the game. The Changelog has an entry for every release.

A patch publishes straight to the latest dist-tag. A breaking release goes through a canary:

  1. The change is announced, and its notice period starts.
  2. The new version publishes to npm under the canary dist-tag, as 0.6.0-canary.N, and the staging host runs it. latest and the production host stay on the old version.
  3. Game devs ship builds that handle both hosts.
  4. On switch day, 0.6.0 publishes under latest and the production host switches to it.
  5. Game devs remove the old path.

During a notice period, do this:

  1. Install the canary of the SDK and the CLI:

    Terminal window
    npm install @chain-protocol/casino-sdk@canary
    npm install --save-dev @chain-protocol/cli@canary

    With Bun:

    Terminal window
    bun add @chain-protocol/casino-sdk@canary
    bun add --dev @chain-protocol/cli@canary
  2. Branch with hostSupports(snapshot, '0.6.0') wherever the release changes what your game sends or reads. Keep the old path in the else branch.

  3. Play the new path with chain dev casino. The SDK simulator is the canary’s, so it answers as the new host. It does not emulate older hosts. To play the old path, submit the build on production and open its test environment, which runs production’s SDK version.

  4. Ship the build before switch day. Until the switch, the production host reports the old version and your game takes the old path.

  5. After the switch, install the release (npm install @chain-protocol/casino-sdk@latest and npm install --save-dev @chain-protocol/cli@latest, or bun add with @latest), remove the old path and ship again.

Snapshot update semantics (read this before animating anything)

Section titled “Snapshot update semantics (read this before animating anything)”

guestApi.setState fires on every host-side state change — balances, locale, viewport, session rows — not only on your session’s transitions. The host deduplicates byte-identical snapshots, but distinct snapshots where nothing about your session changed are normal. Treat setState as “here is the current state”, never as “something happened”:

  • Derive, don’t accumulate. Render each session from its current raw.gameState (plus phaseName / payout). Compare against what you last rendered for that sessionId and animate only the difference. Never replay a session’s presentation from scratch because a new snapshot arrived.
  • A session is new only when its sessionId is new. Phase changes within a known session are progress, never a fresh round.
  • Mid-round WAITING_RANDOMNESS is normal for multi-action games. Every action that consumes randomness (blackjack hit/double/split/stand, a mines reveal) moves the session WAITING_PLAYER_ACTION → WAITING_RANDOMNESS → WAITING_PLAYER_ACTION | SETTLED. Only the session’s first WAITING_RANDOMNESS (before any gameState you have rendered) is the opening deal. Do not reset the table when a round you are already rendering enters WAITING_RANDOMNESS — show a waiting affordance (dealer thinking, tile flipping) and keep the rendered state. The gameState bytes accompanying that transition already include the accepted action.
  • isSettled flips once, atomically with payout and the final gameState; present the outcome from that final state, then call revealOutcome.

Compatibility note: the production host currently suppresses mid-round WAITING_RANDOMNESS states (a round appears to sit in WAITING_PLAYER_ACTION until it settles). This is legacy behavior slated for removal; the local simulator already delivers the full stream. Build and test against the simulator — a game correct there is correct on both.

openSession rejects a wager above the vault’s bet-size cap for that bet configuration, and one the vault’s reserve cannot take right now. The cap moves with the vault and follows from your game’s quotes, so do not compute it in the game: ask the host, and pass the game data you are about to open the session with.

const answer = await hostApi.getMaxWager({ gameData });
// 'limit' → cap the bet input / "Max" button at min(BigInt(answer.maxWager), balance)
// 'no-limit' → the host caps nothing for this bet; balance is the bound
// 'unknown' → the host could not work a maximum out; fall back to your own limits, never to unlimited
  • maxWager is a decimal string in token base units, like the wager of openSession.
  • A limit from the production host is the bet-size cap of that bet configuration, lowered to what the vault’s reserve can take with the sessions open now. What sets the cap is in CONTRACT_CONSTRAINTS.md § Bet-size cap.
  • The production host answers 1% under that figure, rounded down, because the cap falls a little every second and a wager at the exact figure would be refused a block later.
  • Without game data (hostApi.getMaxWager()) the host answers for empty game data, your game’s base configuration. A game with one bet configuration needs nothing more. A game whose quotes reject empty game data gets unknown, and so does a game whose quote fails a guardrail.
  • The answer belongs to one bet configuration. Ask again whenever the player changes it, and on an interval, because the answer moves with the vault.
  • Keep the bet button disabled until the host has answered for the selected configuration.
  • The host answers for the game it serves. The request names no game.
  • Treat a rejected call as unknown.
  • The answer is an estimate. Admission on chain is the authority.

The coinflip example (chain init casino) does this in src/lib/useMaxWager.ts. The local simulator answers with the fixed cap you set in its setup panel; see Local Simulator.


The TypeScript type CasinoGameManifest is minimal; the host validator requires extra fields: presentation and capabilities. Use the validator below when building your static manifest.

Each locales[<locale>].description is the catalog’s game description: optional, at most 500 characters per locale, and shown in the player’s locale with a fallback to defaultLocale. The platform reads it from your build’s preview deployment; see “Game description” in Visual & UX.

assets.coverUrl is the catalog cover image: a path relative to the manifest or an absolute https:// URL to a PNG, JPEG or WebP image of at most 2 MB, 2:3 portrait and at least 230×345. The platform copies it into its storage at build time; see “Cover image” in Visual & UX.

5.1 canonicalCasinoGameId and validateCasinoGameManifest

Section titled “5.1 canonicalCasinoGameId and validateCasinoGameManifest”
import type {
CasinoGameManifest,
CasinoGameManifestValidationResult,
GameManifestMetadata,
} from './types';
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const hasString = (value: unknown): value is string =>
typeof value === 'string' && value.length > 0;
/** Matches on-chain normalizeGameName / manifest gameId conventions (strip trailing "Game", lowercase, alnum only). */
export const canonicalCasinoGameId = (value: string | undefined | null): string => {
if (!value) {
return '';
}
let trimmed = value.trim();
if (trimmed.match(/Game$/i)) {
trimmed = trimmed.replace(/Game$/i, '');
}
return trimmed.toLowerCase().replace(/[^a-z0-9]/g, '');
};
const hasOptionalString = (value: unknown): value is string | undefined =>
value === undefined || typeof value === 'string';
/** Minimal validation for routing + catalog; full shape still typed as CasinoGameManifest. */
export const validateCasinoGameManifest = (value: unknown): CasinoGameManifestValidationResult => {
if (!isRecord(value)) {
return { ok: false, reason: 'Manifest must be an object.' };
}
const { schemaVersion, gameId, defaultLocale, locales, presentation, capabilities } = value;
if (schemaVersion !== 1) {
return { ok: false, reason: 'Unsupported manifest schemaVersion.' };
}
if (!hasString(gameId) || !hasString(defaultLocale)) {
return { ok: false, reason: 'Manifest gameId and defaultLocale are required.' };
}
if (!isRecord(locales) || Object.keys(locales).length === 0) {
return { ok: false, reason: 'Manifest locales must contain at least one locale.' };
}
const defaultLocaleEntry = locales[defaultLocale];
if (!isRecord(defaultLocaleEntry) || !hasString(defaultLocaleEntry.name)) {
return { ok: false, reason: 'Manifest defaultLocale must exist in locales.' };
}
if (
!isRecord(presentation) ||
(presentation.mode !== 'full-iframe' && presentation.mode !== 'embedded') ||
!isRecord(presentation.hostPanels) ||
typeof presentation.hostPanels.openSession !== 'boolean' ||
typeof presentation.hostPanels.history !== 'boolean' ||
typeof presentation.hostPanels.status !== 'boolean'
) {
return { ok: false, reason: 'Manifest presentation is invalid.' };
}
if (
!isRecord(capabilities) ||
capabilities.openSession !== true ||
typeof capabilities.submitAction !== 'boolean' ||
typeof capabilities.forfeitExpiredSession !== 'boolean' ||
typeof capabilities.cancelStuckRandomness !== 'boolean' ||
typeof capabilities.resize !== 'boolean'
) {
return { ok: false, reason: 'Manifest capabilities are invalid.' };
}
const assets = value.assets;
if (assets !== undefined) {
if (
!isRecord(assets) ||
!hasOptionalString(assets.iconUrl) ||
!hasOptionalString(assets.coverUrl)
) {
return { ok: false, reason: 'Manifest assets are invalid.' };
}
}
return {
ok: true,
manifest: value as CasinoGameManifest,
};
};
export const resolveManifestMetadata = (
manifest: CasinoGameManifest,
requestedLocale: string,
): GameManifestMetadata => {
const locale =
manifest.locales[requestedLocale] !== undefined ? requestedLocale : manifest.defaultLocale;
const localized = manifest.locales[locale] ?? manifest.locales[manifest.defaultLocale];
return {
locale,
name: localized.name,
description: localized.description,
iconUrl: manifest.assets?.iconUrl,
coverUrl: manifest.assets?.coverUrl,
};
};
export const assertSameOriginUrls = (manifestUrl: string, iframeUrl: string): boolean => {
try {
const manifestOrigin = new URL(manifestUrl).origin;
const iframeOrigin = new URL(iframeUrl).origin;
return manifestOrigin === iframeOrigin;
} catch {
return false;
}
};

Blackjack — multi-action, forfeit + cancel RNG:

{
"schemaVersion": 1,
"gameId": "BlackjackGame",
"defaultLocale": "en",
"locales": {
"en": {
"name": "Blackjack",
"description": "Render and control the active blackjack table using host-provided on-chain session state."
}
},
"presentation": {
"mode": "full-iframe",
"hostPanels": {
"openSession": false,
"history": false,
"status": false
}
},
"capabilities": {
"openSession": true,
"submitAction": true,
"forfeitExpiredSession": true,
"cancelStuckRandomness": true,
"resize": true
}
}

Mines — multi-action, cancel stuck randomness:

{
"schemaVersion": 1,
"gameId": "MinesGame",
"defaultLocale": "en",
"locales": {
"en": {
"name": "Mines",
"description": "Reveal safe tiles on a 5x5 grid and cash out before the next reveal hits a mine."
}
},
"presentation": {
"mode": "full-iframe",
"hostPanels": {
"openSession": false,
"history": false,
"status": false
}
},
"capabilities": {
"openSession": true,
"submitAction": true,
"forfeitExpiredSession": false,
"cancelStuckRandomness": true,
"resize": true
}
}

Coinflip — instant round (no submitAction in the iframe):

{
"schemaVersion": 1,
"gameId": "CoinflipGame",
"defaultLocale": "en",
"locales": {
"en": {
"name": "Coinflip",
"description": "Flip one or more coins and settle the round from host-provided on-chain state."
}
},
"presentation": {
"mode": "full-iframe",
"hostPanels": {
"openSession": false,
"history": false,
"status": false
}
},
"capabilities": {
"openSession": true,
"submitAction": false,
"forfeitExpiredSession": false,
"cancelStuckRandomness": false,
"resize": true
}
}

6. Frontend patterns (reference implementations)

Section titled “6. Frontend patterns (reference implementations)”

Use viem for encodeAbiParameters / decodeAbiParameters / formatUnits / parseUnits. The bridge’s randomnessRequestData field is deprecated and ignored — omit it.

import { useEffect, useState } from 'react';
import {
connectGameToHost,
type GuestApi,
type HostApi,
type HostSnapshot,
} from '@chain-protocol/casino-sdk/guest';
export function App() {
const [hostApi, setHostApi] = useState<HostApi | null>(null);
const [snapshot, setSnapshot] = useState<HostSnapshot | null>(null);
useEffect(() => {
let mounted = true;
const guestMethods: GuestApi = {
async setState(nextSnapshot) {
if (!mounted) {
return;
}
setSnapshot(nextSnapshot);
},
};
const connection = connectGameToHost(guestMethods);
void connection.promise
.then(parent => {
if (mounted) {
setHostApi(parent);
}
})
.catch(() => {
/* handle */
});
return () => {
mounted = false;
connection.destroy();
};
}, []);
if (!hostApi || !snapshot) {
return <section>Connecting to host…</section>;
}
return null;
}

6.2 Coinflip (instant — openSession only)

Section titled “6.2 Coinflip (instant — openSession only)”

Encode gameData to match your CoinflipGame ABI, e.g. (bool pickHeads, uint8 coinCount, uint8 minWins):

async function handleOpenSession(
hostApi: HostApi,
snapshot: HostSnapshot,
wagerInput: string,
side: 'heads' | 'tails',
coinCount: number,
minWins: number,
) {
const decimals = snapshot.token.decimals ?? 6;
const wager = parseUnits(wagerInput, decimals).toString();
const gameData = encodeAbiParameters(
[{ type: 'bool' }, { type: 'uint8' }, { type: 'uint8' }],
[side === 'heads', coinCount, minWins],
);
const { sessionKey } = await hostApi.openSession({
wager,
gameData,
});
// Track sessionKey until snapshot lists the session and outcome is known
}

Decode raw.gameData / raw.gameState for display; for coinflip, randomness often appears inside gameState after the contract encodes it (game-specific ABI).

6.3 Blackjack (multi-action + approvalAmount)

Section titled “6.3 Blackjack (multi-action + approvalAmount)”

Open with rules packed in gameData (example: uint8 blackjack payout mode + bool dealer hits soft 17):

const PAYOUT_THREE_TO_TWO = 0;
const DEFAULT_DEALER_HITS_SOFT_17 = false;
async function handleOpenSession(hostApi: HostApi, snapshot: HostSnapshot, wagerInput: string) {
const wager = parseUnits(wagerInput, snapshot.token.decimals ?? 6).toString();
const gameData = encodeAbiParameters(
[{ type: 'uint8' }, { type: 'bool' }],
[PAYOUT_THREE_TO_TWO, DEFAULT_DEALER_HITS_SOFT_17],
);
await hostApi.openSession({
wager,
gameData,
});
}
async function handleAction(
hostApi: HostApi,
activeSession: HostSnapshot['sessions']['items'][number],
actionCode: number,
requiresExtraStake: boolean,
) {
await hostApi.submitAction({
sessionId: activeSession.sessionId,
actionData: encodeAbiParameters([{ type: 'uint8' }], [actionCode]),
approvalAmount: requiresExtraStake ? activeSession.wager : undefined,
});
}

approvalAmount: when the player doubles or splits, the facet may need an additional escrowDelta; the host batches ERC20 approve(proxy, amount) + submitAction. Pass the base wager string as approvalAmount when your UI enables those actions.

6.4 Mines (multi-action + maximum wager UX)

Section titled “6.4 Mines (multi-action + maximum wager UX)”

Open round with mine count:

async function handleOpenSession(
hostApi: HostApi,
snapshot: HostSnapshot,
wagerInput: string,
mineCount: number,
) {
const wager = parseUnits(wagerInput, snapshot.token.decimals ?? 6).toString();
const gameData = encodeAbiParameters([{ type: 'uint8' }], [mineCount]);
await hostApi.openSession({
wager,
gameData,
});
}
const ACTION_REVEAL = 0;
const ACTION_CASHOUT = 1;
async function handleReveal(
hostApi: HostApi,
activeSession: HostSnapshot['sessions']['items'][number],
tileIndex: number,
) {
await hostApi.submitAction({
sessionId: activeSession.sessionId,
actionData: encodeAbiParameters(
[{ type: 'uint8' }, { type: 'uint8' }],
[ACTION_REVEAL, tileIndex],
),
});
}
async function handleCashout(
hostApi: HostApi,
activeSession: HostSnapshot['sessions']['items'][number],
) {
await hostApi.submitAction({
sessionId: activeSession.sessionId,
actionData: encodeAbiParameters([{ type: 'uint8' }, { type: 'uint8' }], [ACTION_CASHOUT, 0]),
});
}

Maximum wager: ask the host with hostApi.getMaxWager({ gameData }) for the chosen mines count and disable Open above a limit answer before the user hits chain. A wager above its bet-size cap reverts with CasinoGameFacet__WagerAboveRiskBudget, CasinoGameFacet__WagerAboveBreakEvenRule or CasinoGameFacet__NetWinAboveSingleWinCap. A wager the vault’s reserve cannot take reverts with CasinoGameFacet__InsufficientPortfolioReserve. Map each to a friendly message.

Stuck randomness: optional cancelStuckRandomness({ sessionId }) when phaseName === 'WAITING_RANDOMNESS' and the host exposes recovery (mines manifest sets cancelStuckRandomness: true).


7. Host integrator checklist (signing + snapshot)

Section titled “7. Host integrator checklist (signing + snapshot)”

If you implement the parent app (not the iframe), you must:

  1. Approve + openSession: Encode CasinoGameFacet.openSession(gameAddress, vault, wager, gameData). Prepend ERC20 approve(facetOrProxy, wager) from the player’s funding path (EOA or Smart Vault batch).
  2. Parse sessionId and the snapshot: From CasinoSessionOpened logs after the transaction, build sessionKey = \${chainId}:${sessionId}`; keep the encoded session bytes of the newest **CasinoSessionAdvanced`** log per session — the facet stores only their hash.
  3. submitAction: Optionally prepend approve when the guest passes approvalAmount; then submitAction(encodedSession, actionData) with the latest committed snapshot (own receipts, flashblock push or indexed feed, whichever is newest).
  4. Snapshot freshness: Decode the committed session bytes (CasinoSessionCodec, or CasinoConfigFacet.decodeSession) so raw.gameData and raw.gameState match on-chain bytes for decoding in the iframe.
  5. Push updates: Call guestApi.setState whenever wallet, token balances, session rows, locale, theme, or viewport change.
  6. Maximum wager: Answer getMaxWager from CasinoConfigFacet.getMaximumWager(vault, gameAddress, gameData) for the game you serve, with empty game data when the guest passes none. The view returns maximumWager in token base units, the limit that binds (0 risk budget, 1 break-even rule, 2 single-win cap, 3 the reserve, when it can take less than the bet-size cap) and the current LP cut; answer with maximumWager less a headroom of 1%, rounded down, because the figure falls every second. Answer unknown when the view reverts.

An operator is a frontend that serves players on the shared vaults. It earns the operator fee of every session attributed to it: what is left of the session’s net edge after the LP cut and the protocol fee (§2.1.1). The operator code is simply the operator’s address.

Status: operator registration is closed. Only the protocol’s own operator is registered today, so every session is attributed to it and nothing changes for your game or your host. Registration will open to other frontends in a future release; this section describes the interface that is already live so hosts can be built against it.

  • Naming the operator: the facet has a second entry point, openSession(game, vault, wager, gameData, operator). The four-argument call keeps working and means “no operator code”.
  • No code, or an unregistered code, never reverts. The session is attributed to the protocol’s default operator instead, so a stale host config cannot block a bet.
  • The code cannot be swapped in transit. It is part of the openSession calldata, which the player’s Smart Vault signs as one batch. A relayer that changes it invalidates the signature.
  • Gas sponsorship: the chain.wtf relayer pays gas only for sessions that name no operator or chain.wtf’s own. An operator sponsors its own players by running its own relayer.
  • Games see nothing. The operator is not part of SessionContext and does not reach the iframe. It is committed inside the encoded session (20 bytes after the risk flags), followed by the session’s edge and the three rates its fees are computed from. The fixed part of the encoded session is 295 bytes, so a host that decodes session bytes itself must use the current CasinoSessionCodec layout.

8.1 Guest package.json dependencies (example)

Section titled “8.1 Guest package.json dependencies (example)”
{
"dependencies": {
"@chain-protocol/casino-sdk": "0.5.0",
"react": "^19.0.0",
"viem": "^2.x"
},
"devDependencies": {
"@chain-protocol/cli": "0.5.0"
}
}

Keep the SDK and the CLI on the same version range, as chain init writes them. The SDK brings penpal as its own dependency. See SDK versions and breaking releases for what a version range takes.

  • Penpal origin allowlisting: the host passes childOrigin; the guest allows the referrer origin when document.referrer is parseable, else * (development only — production games should be served from a stable origin behind HTTPS).
  • Never trust client-only math for payouts — always treat gameState and balances as hints; the facet + game contract are authoritative.

End of document.

The examples above illustrate selected interfaces; they are not complete signatures. Consult the declarations installed with the SDK for the full exports and signatures for your installed version.

  1. @chain-protocol/casino-sdk: shared types such as HostSnapshot and HostApi, manifest validation, deployment helpers and hostSupports.
  2. @chain-protocol/casino-sdk/guest: connectGameToHost, reportGameContentSize, observeGameContentSize and guest-facing types.
  3. @chain-protocol/casino-sdk/slot-engine and /graph-engine: engine helpers; /slot-engine/threshold exposes threshold helpers.
  4. @chain-protocol/casino-sdk/contracts/*, /slot-engine/contracts/* and /graph-engine/contracts/*: Solidity contracts.

The host, simulator and conformance entry points are internal and outside semver. See package entry points for context.