Turn chance draws, player decisions, and prize tables into a TypeScript game graph. The shared engine walks it on-chain, so multi-step casino games need no custom Solidity.
The graph engine runs multi-step casino games without a contract of your own. You write the game as a TypeScript state machine: chance draws, player decisions, prize tables and fair ladders. The compiler turns it into a game graph, and one shared contract plays every session by walking that graph. The chain derives the RTP, the maximum payout and the risk figures from the graph before any session opens.
Prefer an engine to your own contract. A graph-engine title needs no Solidity and no contract
audit. The chain checks its math when it deploys, so the platform review covers your frontend
only. A title whose highest RTP is at or below the approval ceiling (98% at genesis) is
whitelisted as soon as it deploys and joins the offering right away. Write your own ICasinoGameV2 contract only for
a game neither engine can express (see Getting Started).
| Engine | Fits |
|---|---|
| Slot engine | One draw settles the bet: slots, scratch cards, Limbo, dice over or under a target. |
| Graph engine | Several steps or player choices: Hilo, a tower climb, blackjack, double-or-nothing, roulette, Plinko. |
The package is @chain-protocol/casino-sdk/graph-engine. It holds the compiler
(compileGraphGame), the reference walks your frontend replays sessions with, the quote helpers
and the deploy helpers. @chain-protocol/casino-sdk/graph-engine/contracts/* holds the engine
contracts as Solidity source. From 0.6.0 the TypeScript API calls a title a Game, so
the definition type is GraphGameDefinition. The Solidity contracts and the deployment.ts field
title keep their names.
Build your own title
Section titled “Build your own title”Run every command below in your game’s project. A project created with
npx @chain-protocol/cli init casino already has the casino SDK and the CLI (see
Getting Started). Otherwise install the same version of both:
npm install @chain-protocol/casino-sdknpm install --save-dev @chain-protocol/cliWith Bun, run bun add @chain-protocol/casino-sdk and bun add --dev @chain-protocol/cli.
Step 1: write the title
Section titled “Step 1: write the title”A title is a GraphGameDefinition. It has four fields:
prizeDenominator. Prizes are multiples of the wager in units of1 / prizeDenominator. With10, a prize of19pays 1.9x.roots. One starting state per bet configuration, in the order game data selects them.key. A unique string for each state. The compiler merges states with the same key.expand. The node a state is. The compiler calls it on every state it reaches.
This title is Climb, a three-rung tower. Each climb survives half the time. The player cashes out for 1.9x at rung 1, 3.9x at rung 2 or 7.6x at rung 3. It is the tower of the Climb example, which you can play with a full frontend:
import type { GraphGameDefinition, NodeSpec } from '@chain-protocol/casino-sdk/graph-engine';
export const LABEL = { cashOut: 0, climb: 1, safe: 2, bust: 3 } as const;const CASH_OUT_PRIZES = [0, 19, 39, 76];const TOP_RUNG = 3;
type State = | { at: 'climb'; rung: number } | { at: 'rung'; rung: number } | { at: 'cashOut'; rung: number } | { at: 'bust' };
const climb: GraphGameDefinition<State> = { prizeDenominator: 10, roots: [{ at: 'climb', rung: 0 }], key: state => JSON.stringify(state), expand: (state): NodeSpec<State> => { switch (state.at) { case 'climb': return { kind: 'chance', outcomes: [ { state: { at: 'rung', rung: state.rung + 1 }, weight: 1, label: LABEL.safe }, { state: { at: 'bust' }, weight: 1, label: LABEL.bust }, ], }; case 'rung': { const cashOut = { state: { at: 'cashOut', rung: state.rung } as const, label: LABEL.cashOut, }; if (state.rung === TOP_RUNG) return { kind: 'decision', actions: [cashOut] }; return { kind: 'decision', actions: [cashOut, { state: { at: 'climb', rung: state.rung }, label: LABEL.climb }], }; } case 'cashOut': return { kind: 'terminal', prize: CASH_OUT_PRIZES[state.rung] }; case 'bust': return { kind: 'terminal', prize: 0 }; } },};
export default climb;expand returns one of five node kinds:
| Kind | What it is |
|---|---|
terminal | Settles the session at prize. |
chance | Draws one of its outcomes by weight and moves to that outcome’s state. |
table | Draws one prize from weighted tiers and settles at once. Use it for a one-draw payout table. |
decision | Waits for the player to pick one of its actions. Action 0 is the default when a session forfeits. |
ladder | A fair ladder: a whole Hilo-style game in one node. See Fair ladders. |
Every chance outcome, table tier and decision action carries a 32-bit label. The engine reports
labels, not states, so your frontend renders what happened from them. Keep one label per meaning,
such as LABEL.bust, and share the constant between the title and the frontend.
A terminal or table may set stake, the number of wagers escrowed when it settles (1 to 256).
A player action that reaches a deeper stake, such as a double, escrows the difference. A random
draw cannot escrow, so every outcome of a chance must hold the same stake. A decision holds the
stake of its action 0.
To offer several bet configurations, list several roots. A dice title, for example, has one root per target. No root may be reachable from an earlier root or share its state. Choices that only change what the frontend shows belong in the game data’s tail, not in a root.
Step 2: check the figures
Section titled “Step 2: check the figures”compileGraphGame(title) enumerates every reachable state, derives the figures the chain will
derive, and packs the chunks it deploys. A title that breaks a rule throws, naming the node or the
bet configuration at fault. Print each configuration’s RTP and maximum payout:
import { compileGraphGame } from '@chain-protocol/casino-sdk/graph-engine';import climb from './title.ts';
const { graph, derived } = compileGraphGame(climb);for (const [index, figures] of derived.configurations.entries()) { const rtp = (Number(figures.rtpWad) / 1e16).toFixed(2); const maxPayout = Number(figures.maxPayout) / Number(graph.prizeDenominator); console.log(`configuration ${index}: RTP ${rtp}%, max payout ${maxPayout}x`);}For Climb, expect configuration 0: RTP 97.50%, max payout 7.6x. The RTP assumes best play at
every decision, so it is the most any player strategy returns. Best play climbs from rung 1 and
cashes out at rung 2.
Step 3: play it locally
Section titled “Step 3: play it locally”Point deployment.ts in the project’s root at the title module, and name the title:
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default { kind: 'graph', name: 'Climb', title: 'title.ts',} satisfies DeploymentInput;Serve your frontend, then start the SDK simulator:
npx chain dev casino --frontend-url http://localhost:5173Expect Deployed Climb at 0x… and then Play your game at …. Open that URL, pick Climb in the
game list and play. Every session runs on the real engine bytecode, with randomness from the local
VRF node. The simulator watches the title and every module it imports. Save a change and it
deploys the title again. The simulator needs Node 22.18 or later, or Bun, to load a TypeScript
title. See Local Simulator → Graph titles.
Step 4: submit it
Section titled “Step 4: submit it”Submit the repository through the developer platform. The build loads deployment.ts, runs your
title module through the platform’s graph compiler without network, and records the chunks and
the title inputs hash. It compiles no contract, because a graph title has none. The verify step
deploys the title on an in-memory chain and derives it to the end. A title whose derive reverts
fails the build, naming the revert. The verdict reports each bet configuration’s RTP and maximum
payout.
The review then covers your frontend only. Every title runs on the same engine contract, and the chain refuses a title whose math breaks a rule, so nobody reviews your math by hand. When the title deploys, it registers on the diamond. A title whose highest RTP is at or below the approval ceiling (98% at genesis) is whitelisted at once. A title above the ceiling waits as a pending game until the security council approves it.
A title with a Steering contract builds the same way. The platform also
compiles the Steering contract from contracts/ and hashes the title with the address where
SteeringDeployer will create it. When the title deploys, the Steering contract deploys first.
Build the frontend
Section titled “Build the frontend”The frontend is a normal casino SDK guest. Read Building Casino Games for the bridge, the snapshot and the session lifecycle. This section covers what is specific to graph titles.
The frontend needs the compiled graph to replay sessions. Import your title module and call
compileGraphGame once, or compile at build time and ship the result. The manifest field of
the compiled title maps every node id back to your own state type, which is often the easiest
thing to render from. The engine helpers have no Node imports, and a guest that walks, encodes
and quotes a title bundles them in about 19 KB minified.
- Open a session. The game data names the bet configuration. Empty game data opens
configuration 0.
encodePlainGameData(configuration, tail)opens another. The engine ignores the tail, which can carry display-only choices such as a dice side. - Read where the session stands. The session’s
gameStatestarts with the current node id as 4 bytes. Without a Steering contract, nothing follows it, except at a fair ladder. - Act at a decision. Call
submitActionwithactionData = abi.encode(uint256 actionIndex), the index into the decision’sactions. Each action requests a randomness word, and the randomness callback walks the chance nodes that follow. - Show what happened. Replay each randomness word with
walkRandomness(graph, from, word), wherefromis the node the session stood at. It follows the engine draw for draw and returns the labels of every edge it took and the node it stopped at. Render each label. - Quote a bet without a chain call.
graphQuoteTable(compiled)gives the figures the engine quotes from. Build it at build time and ship it. In the browser,quoteConfiguration(table, configuration, wager)returns what the engine’squoteCaps,quoteRiskParamsand opening reserve give, bit for bit. Still ask the host for the maximum wager withhostApi.getMaxWager({ gameData }).
The frontend never decides an outcome or a payout. The engine settles every session from the graph and the randomness.
The example: Climb
Section titled “The example: Climb”Climb is a complete graph-engine game with a frontend. It lives in the Chain repository at
packages/casino-sdk/graph-engine/example/, not in the npm package. It has two bet
configurations: the tower from Step 1, at 97.5% RTP, and a one-draw
scratch card, at 76%.
ladder.tsis the title. The frontend, the deploy script and the end-to-end test import the same module, so the chain, the replay and the UI share one definition.start.tsdeploys Climb on the SDK simulator’s chain and lists it in the game picker.frontend/is the guest iframe. It compiles the title in the browser withcompileGraphGame, opens a tower or scratch-card session withencodePlainGameData, and reads the current node from the first 4 bytes ofgameState. At a rung it offers Cash out (action 0) and Climb (action 1) throughsubmitAction. It replays every randomness word withwalkRandomnessto show each draw, and callsrevealOutcomewhen a session settles.frontend/src/climb.tsholds the replay.
To run it from a checkout of the Chain repository, in packages/casino-sdk:
bun installbun run start:graphExpect Climb 0x… RTP tower 97.50%, scratch card 76.00%, then
open http://localhost:3300/?game=http://localhost:3600 and pick Climb in the game list. Open that
URL, pick Climb in the game list and press Restart harness. Then climb the tower or scratch
a card. The frontend shows the rung you stand on, every draw (safe, bust, or the scratch card’s
tier) and the payout.
bun run graph:e2e plays 10 sessions against the running stack and expects
10 sessions opened, played and settled as the reference walk predicts.
Fair ladders
Section titled “Fair ladders”A fair ladder is one node that plays a whole climb-and-cash-out game, such as Hilo. It holds its own rungs and a banked multiplier. Every win scales the multiplier by a drawn ratio, and a cash-out pays the multiplier times the ladder’s base prize, up to its cap. A ladder keeps the graph small: Hilo is one ladder of 26 rungs instead of thousands of nodes.
- Write the ladder as a
LadderNodethatexpandreturns:basePrizeandcapPrizeover the title’s prize denominator,ratioDenominator, theentryoutcomes and therungs. Each rung lists its options and whether the player may cash out there. Each option lists its outcomes. Each outcome has a weight, a ratio (0 loses), the rung it moves to and a label. - Fairness. Derive checks that every option is fair to within one part in 10,000, so best
play returns within 2 basis points of the ladder’s value. It also requires
2 × basePrize <= capPrize, and that no entry outcome reaches the cap. - Play. At a rung,
actionDataisabi.encode(uint256 index). An option index draws that option, and the option count cashes out. The engine refuses an option one of whose wins would bring the cash-out to the cap.walkLadder(graph, node, word, actions, wager)replays a session, anddecodeLadderState(gameState)reads the rung and the banked multiplier. - Auto-bet.
encodeCommittedLadderGameData({ configuration, options }, tail)opens a session that plays every listed option in one randomness callback and settles. The session reserves what the whole chain can win when it opens. A configuration takes at mostcommittedRoundsoptions, a figure derive sets: 93 for Hilo.walkCommittedLadder(graph, rounds, word, wager)replays the callback.
A rung holds at most 254 options and 256 outcomes over all of them.
Table bundles
Section titled “Table bundles”A table bundle is one session over several single-table bet configurations, such as several roulette chips or several Plinko balls. The wager splits across the entries by units, and the session settles once.
- Declare which configurations combine, in the
GraphGameDefinition.drawGroupslists groups that share one draw: every table of a group has the same tier weights and labels, so only the prizes differ.maxIndependentDraws(1 to 16) andindependentlyDrawabledeclare configurations that draw independently. Every bundled configuration’s root must be a table of one wager. - Open a session with
encodeBundleGameData({ draw, entries }, tail), wheredrawis'shared'or'independent'and each entry is{ configuration, units }. A shared bundle takes 1 to 64 entries in ascending order from one draw group. An independent bundle takes up tomaxIndependentDrawsentries, repeats allowed.checkBundleruns the engine’s checks off chain. - Show the result with
walkBundle(graph, bundle, word, wager), which rebuilds each entry’s label and prize. A bundle keeps an empty game state, so read the entries back from the session’s game data withdecodeBundleGameData. - Quote a bundle with
quoteBundle(bundleFigures(graph, derived, bundle), wager).
A title with a Steering contract cannot declare bundles.
Steering contracts
Section titled “Steering contracts”A title may name an immutable Steering contract that implements
@chain-protocol/casino-sdk/graph-engine/contracts/IGraphSteering.sol. Use one when most of a
game’s state never changes the money, such as positions on a 9×9 board. The graph then holds only
what changes the prize, and the Steering contract holds everything else. act turns the player’s
actionData into an action of the current decision. respond may force an action when a
randomness walk stops at a decision. If respond fails, the engine takes action 0 and carries
on, so a Steering contract can never block a settlement.
Derive takes the best action at every decision, so no Steering contract can pay more than the derived RTP. That RTP is a ceiling, not what a typical player earns, so measure the gap by simulation before you ship. A steered title reserves its whole cap when a session opens, and it cannot hold a fair ladder or declare bundles.
Name the Steering contract in deployment.ts as steering: { source, name, constructorArgs }.
source is relative to contracts/, and constructorArgs is optional and takes the
{{diamond}} and {{token}} placeholders. The SDK simulator deploys it directly. The developer
platform creates it through SteeringDeployer, which uses CREATE2 with a zero salt, so its
address depends only on the init code. Titles with the same Steering init code share one Steering
contract. That is safe because IGraphSteering is view and the session’s gameState holds the
steering state. A constructor must not read chain state. If it does, the code at that address can
differ from the reviewed build’s code, and the deploy then stops and asks for a rebuild. The
platform refuses a steered title with a fair ladder or a bundle when it builds the title.
Rules the chain enforces
Section titled “Rules the chain enforces”- RTP. Derive computes every configuration’s value from the graph and rejects one at or above 100% RTP. Best play at every decision counts. The title then registers on the diamond with the lowest and highest RTP of its configurations. The diamond refuses a title outside the council’s RTP band.
- Approval. A title whose highest RTP is at or below the approval ceiling is whitelisted at once. The ceiling is 98% at genesis, and the council can change it. A title above it is a pending game until the security council approves it.
- Game developer. The creation call names the game developer, who collects the game dev fee.
Anyone may name any developer, since the name only directs the fee.
deployGraphGametakes it asdeveloperand names the signing account without one. An authorship is an optional way for a developer’s wallet to authorize a specific creator wallet: that wallet writes an authorship for the title inputs hash and the creator, the creator passes its id to the creation call asauthorshipId, and the named developer must be the authorship’s writer. - Structure. Every weight is above zero. A run of chance nodes is at most 16 long, so one randomness word covers it. Every outcome of a chance holds the chance’s stake. A decision has at most 256 actions. A title has at most 65,535 bet configurations and 1,224 chunks.
- Forfeit. A session that forfeits at a decision pays the decision’s default value, less any part of its action 0’s stake the session has not escrowed.
- Risk. The quotes and the maximum payout come from the derived figures, never from the frontend or the game data. A session reserves what its next step can win, so a long game ties up little liquidity at its start. Your fees and your maximum wager follow from those quotes. See Contract Constraints → What your quotes decide.
Deploy cost
Section titled “Deploy cost”A title deploys in one transaction when its derive fits the 2²⁴ per-transaction gas cap, which holds about 4,400 nodes or about 300 single-table configurations. A larger title derives in batches of calls under 14M gas each, and anyone may send them. The figures below are measured on the local casino host.
| Title | Nodes | Configurations | Derive |
|---|---|---|---|
| Hilo, one fair ladder of 26 rungs | 1 | 1 | 2.10M gas of chunks, 0.60M to derive |
| A graph near the one-transaction limit | 4,331 | 1 | inline, 16.0M gas |
| Dice, one configuration per target | 9,800 | 9,800 | batched, 64 calls, 805.8M gas |
| A large graph | 42,083 | 1 | batched, 89 calls, 941.6M gas |
A Hilo session costs about the same gas as a hand-written Hilo contract: about 426k to open and 103k to 107k for each randomness callback.
Deploy from a script
Section titled “Deploy from a script”The developer platform deploys titles for you. To deploy one yourself, for example on a test
chain, call deployGraphGame({ publicClient, walletClient, deployer, compiled }) with the
GraphTitleDeployer address. It uploads the chunks, creates the title naming developer (the
wallet’s account without one) as its game developer, derives it inline or in batches, and checks
that the chain derived the compiler’s figures. createBatchedGraphGame and
deriveGraphGame run the two halves of a batched deploy, and deriveGraphGame resumes from where
the last call stopped.