Skip to content

Getting started: build your first casino game

Go from an empty directory to a coinflip round on your machine, then make the game your own. No account, backend access, or testnet funds required.

This page takes you from an empty directory to a coinflip round played on your machine. Then you replace the coinflip game and frontend with your own. You need no Chain.wtf account, no backend access and no testnet funds.

You need Node.js 22 or newer, or Bun 1.4.2 or newer. Nothing else: the SDK simulator brings its own chain, VRF node and Solidity compiler.

A Chain.wtf casino game is two deliverables plus a small metadata file:

  1. The game’s rules, which settle bets against the house liquidity pool. Write them for one of the two engines, or as your own Solidity contract. See Choose how to write your game.
  2. A static web frontend (any framework, or none) that the Chain.wtf app embeds as a sandboxed iframe. It contains no wallet code. It talks to the host page through the @chain-protocol/casino-sdk bridge, and the host signs everything.
  3. A game.manifest.json served next to your frontend, describing the game to the host.

The flow of a bet: the player configures a wager in your UI, your UI asks the host to open a session, and the host signs the transaction. The game contract requests verifiable randomness (VRF) and settles the outcome. Your UI animates the result and reveals the payout.

Write your game for the slot engine or the graph engine whenever it fits. An engine is a shared game contract that the chain already trusts. You describe your game’s math in a file or a TypeScript module, and the engine plays it. You write no Solidity.

An engine game reaches players faster:

  • Only your frontend is reviewed. The chain derives the RTP, the maximum payout and the risk figures from your game’s math when it deploys, and refuses math that breaks a rule. Nobody reviews or audits a contract, because you ship none.
  • It joins the offering right away. A game whose highest RTP is at or below the approval ceiling (98% at genesis) is whitelisted as soon as it deploys. A game above the ceiling waits for the security council.

Pick the engine that fits your game:

Your gameWrite it with
One draw settles the bet: slots, scratch cards, Limbo, dice over or under a target.Slot engine
Several steps or player choices: Hilo, a tower climb, blackjack, double-or-nothing, roulette, Plinko.Graph engine
Neither engine can express it.Your own ICasinoGameV2 contract (Steps 4 and 5)

Write your own contract only when your game fits neither engine. You then own the contract’s math, its risk quotes and its security. The platform reviews the contract as well as the frontend, and the contract needs an audit. Nothing on chain proves its RTP, so it always waits as a pending game until the security council approves it.

Steps 1 to 3 below are the same for every game. With an engine, follow the engine’s page in place of Steps 4 and 5, then come back for Step 6.

Create a game from the coinflip example with the chain command from @chain-protocol/cli:

Terminal window
npx @chain-protocol/cli init casino my-game

With Bun:

Terminal window
bunx @chain-protocol/cli init casino my-game

Expect Created my-game from the Coinflip example. followed by the next steps. my-game/ holds:

PathWhat it is
contracts/src/CoinflipGame.solThe game contract.
deployment.tsNames the contract the SDK simulator compiles and deploys.
src/The game frontend (React and Vite).
public/game.manifest.jsonThe manifest the host fetches from your frontend’s origin.
package.jsonDepends on @chain-protocol/casino-sdk and @chain-protocol/cli, one version.
Terminal window
cd my-game
npm install

With Bun:

Terminal window
cd my-game
bun install

Start the frontend in one terminal:

Terminal window
npm run dev

With Bun, run bun run dev. Expect Vite to serve the game at http://localhost:3100/.

Start the SDK simulator in a second terminal:

Terminal window
npm run simulator

With Bun, run bun run simulator. The script runs chain dev casino --frontend-url http://localhost:3100. It starts the dev chain, the VRF node and a minimal casino host, compiles and deploys the contract deployment.ts names, and serves the simulator page. Expect these lines when it is ready:

[simulator] Deployed CoinflipGame at 0x…
Play your game at http://127.0.0.1:3300/?game=http%3A%2F%2Flocalhost%3A3100&rpc=…
Press Ctrl-C to stop.

Open the URL it prints. The simulator page mounts your frontend in its iframe, and the player account holds 1,000,000 test chUSD. Pick Heads or Tails and press BET. Expect the coin to land within a few seconds, the result to show in the top-right rail, and session 1 SETTLED in the log under the frame.

The SDK simulator behaves like the production host: optimistic sessions, a lagging indexer feed and balance guarding, against a real VRF node on your machine. Everything you do from here on replaces the coinflip contract and UI with your own.

Skip this step and Step 5 if your game uses an engine. The Slot Engine and Graph Engine pages show how to write and play an engine game.

Implement ICasinoGameV2. Import it from the SDK, as contracts/src/CoinflipGame.sol does:

import { ICasinoGameV2 } from '@chain-protocol/casino-sdk/contracts/ICasinoGameV2.sol';

The functions that matter most:

FunctionWhat it does
onSessionStartValidates the bet (gameData), sets up session state, usually requests randomness.
onRandomnessReceives the VRF result, computes the outcome, settles (or advances a multi-step game).
onPlayerActionOptional. Mid-session moves for multi-action games (hit or stand, reveal a mine, cash out).
quoteCapsThe most stake and the largest net win one session of a bet configuration can reach.
quoteRiskParamsExpected payout, maximum payout, its probability and the body variance of a bet.

Your quotes set your fees and your maximum wager. openSession computes every session’s fees from the edge you quote (wager − expectedPayout), and works out the largest wager it accepts from what quoteRiskParams returns. Get these right before anything else:

  • expectedPayout is RTP × wager, under the player’s best policy when your game has choices.
  • probabilityWad is the probability of exactly maxPayout, not of any win.
  • bodyVarianceScaled is the variance of the payout with the top tier excluded, in wei² × 1e18.

A quote that fails a guardrail reverts openSession. What each figure must mean, the errors, the fee split and the bet-size cap are in Contract Constraints → What your quotes decide.

Two shapes cover almost every game:

  • Instant (coinflip, dice, slots): onSessionStart requests randomness, onRandomness settles. One transaction from the player’s point of view.
  • Multi-action (blackjack, mines): the session stays open across several onPlayerAction calls, each optionally requesting more randomness.

Both patterns, with full contract examples, are in Building Casino Games §2.3.

Two rules worth knowing before you write a line of code:

  • Deriving dice or cards from random bytes: use rejection sampling, never byte % n. See Randomness → Dice. A game gets one random value per request and derives every other value it needs from it; onRandomness may not request randomness again.
  • The facet enforces phase rules, timeouts and payout caps on top of your contract. Read Contract Constraints so your state machine doesn’t fight it. Slots-like games with rare huge payouts also need the tiered reserve model in Slots Risk & Reserves.

deployment.ts tells chain dev which contract to compile and deploy:

import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default {
kind: 'casino',
contract: { source: 'src/MyGame.sol', name: 'MyGame' },
} satisfies DeploymentInput;
  • contract.source is relative to contracts/, and contract.name is the contract in that file.
  • If the constructor takes arguments, list them in constructorArgs. {{diamond}} and {{token}} stand for the host and the token addresses of the chain you deploy to.
  • Keep deployment.ts self-contained: import only types. chain dev loads it on its own, and the platform reads the same file when it deploys your submission.

While npm run simulator runs, it watches deployment.ts and every Solidity file the build read. Save a change and it recompiles, deploys the contract at a fresh address and lists it first in the simulator page’s game picker. You do not restart anything. A compile error prints in the simulator’s terminal, and the previous deployment keeps running.

To compile the contract with your own toolchain as well, Hardhat resolves @chain-protocol/casino-sdk/contracts/… from node_modules. Foundry needs the remapping @chain-protocol/=node_modules/@chain-protocol/.

Start from the example’s src/ or from scratch. The whole bridge is small:

import { connectGameToHost } from '@chain-protocol/casino-sdk/guest';
import type { HostApi, HostSnapshot } from '@chain-protocol/casino-sdk/guest';
const connection = connectGameToHost({
setState: async (snapshot: HostSnapshot | null) => {
// called on every host update: wallet status, balances, session rows
render(snapshot);
},
});
const hostApi: HostApi = await connection.promise;
// Place a bet: ABI-encode your bet config as gameData
const { sessionKey } = await hostApi.openSession({
wager: parseUnits('10', snapshot.token.decimals).toString(),
gameData: encodeAbiParameters(...),
});
// ...watch snapshot.sessions.items for the row with your sessionKey to reach
// a terminal phase, decode raw.gameState, play your result animation, then:
await hostApi.revealOutcome({ sessionId });

The essential guest rules:

  • Only enable betting when snapshot.wallet.status === 'ready'.
  • Clamp the bet input to what the platform accepts right now: hostApi.getMaxWager({ gameData }) answers the maximum wager for the bet the player selected. Ask again when the player changes the bet, and never compute the maximum yourself. See Building Casino Games §4.
  • Match your bet to a session row via the returned sessionKey. The row’s phaseName turning terminal (SETTLED, FORFEITED or CANCELLED) is your settle signal.
  • Always call revealOutcome when your win animation finishes. Until then the host hides the payout from its balance displays so the top bar doesn’t spoil the result.
  • Call connection.destroy() on unmount.

The full lifecycle, snapshot reference, and per-game frontend patterns (coinflip, blackjack, mines) are in Building Casino Games §3–6.

If your frontend runs on another port, pass it to the simulator: npx chain dev casino --frontend-url http://localhost:5173 (bunx chain dev casino … with Bun), or change the simulator script in package.json. The setup panel lets you raise the indexer lag, force wallet states and change the maximum-wager answer. See Local Simulator for the full tour.

Serve a game.manifest.json from the same origin as your game. The example serves it from public/game.manifest.json; adjust it to your game. The gameId must follow the canonical id rules, and the host validates presentation and capabilities. Validate yours locally with validateCasinoGameManifest from the SDK (details).

Before shipping, use the simulator to make sure your game survives production reality:

  • Slow indexer: raise the indexed-feed lag in the setup panel. Your game should ride the optimistic session rows without flicker or duplicate results.
  • Wallet not ready: force disconnected or setup-required and check your non-ready screens.
  • Maximum wager: set the setup panel’s max-wager answer to a low limit, then to unknown. Your bet input should clamp to the limit and fall back to your own limits, never to unlimited.
  • Refresh mid-round: reload the iframe during an open session. Your UI should recover the session from the snapshot instead of starting blank.

Build your frontend with npm run build or bun run build, which writes dist/, and host it at a stable HTTPS URL with game.manifest.json at the same origin. What else you deliver depends on how you wrote the game:

  • With an engine, submit the repository with its deployment.ts through the developer platform. The platform builds your title, the chain checks its math when it deploys, and the review covers your frontend only. A title at or below the approval ceiling is whitelisted at once.
  • With your own contract, you also deliver the audited game contract. The review covers the contract and the frontend, and the security council approves the game before it takes bets.

The Chain.wtf maintainers wire the rest: indexer registration and the catalog entry.

The host tells your game its SDK version in snapshot.sdkVersion. A breaking release reaches staging as a canary before it reaches production, and your game handles both hosts during that notice period. Read Building Casino Games → SDK versions and breaking releases.