Put your casino game through its paces with a local chain, VRF node, and SDK host bridge. Catch balance guards and timing quirks before players do.
chain dev casino runs the casino SDK simulator in your game’s repository. It mounts your game
frontend in the simulator page, which drives it through the same @chain-protocol/casino-sdk
bridge as the production Chain.wtf host. Behind the page it runs its own chain, a minimal casino deployment,
a real Verify Network VRF node and the contract your deployment.ts names. Everything runs on
your machine; no Chain.wtf account or backend access is needed.
If you can make your game fun and robust in the simulator, it will behave the same way in production. The simulator page replicates the host’s quirks (optimistic updates, lagging data feeds, balance guarding) so you hit them on day one instead of after shipping.
Quick start
Section titled “Quick start”The simulator ships inside the SDK your project installed, and the chain command from
@chain-protocol/cli starts it. A game created with chain init casino has both, plus a
simulator script (see Getting Started). To add them to an existing
project, install the same version of each:
npm install @chain-protocol/casino-sdknpm install --save-dev @chain-protocol/cliWith Bun:
bun add @chain-protocol/casino-sdkbun add --dev @chain-protocol/cliServe your frontend, then start the simulator in the project’s root, next to deployment.ts:
npx chain dev casino --frontend-url http://localhost:5173With Bun, run bunx chain dev casino --frontend-url http://localhost:5173. Expect
Play your game at http://127.0.0.1:3300/?game=… when it is ready. Open that URL. Press Ctrl-C
to stop the chain, the VRF node and the simulator page. The simulator page listens on port 3300
and the dev chain on 8545; chain dev options lists the flags that change them.
No chain tooling is required. The dev chain is an in-memory EDR chain inside the simulator’s own process. If a chain already answers on the RPC port, the simulator deploys to it instead.
The player defaults to dev-mnemonic account #0, which holds 1,000,000 test chUSD. Query params override the saved setup:
http://localhost:3300/?game=http://localhost:5173&gameAddress=0x…&rpc=…The simulator is the SDK’s internal ./simulator subpath. It sits outside semver; do not import
it from a game. Because chain dev runs the simulator of the SDK your project installed, the
simulator always matches the SDK version your game builds against.
What chain dev casino runs
Section titled “What chain dev casino runs”- A chain: the in-memory dev chain (EDR) on the RPC port, or the chain already answering there.
- Verify Network VRF: deploys the real router from
@verify-network/local, registers a local fulfilling node (dev account #3) and keeps answering randomness requests with real ECVRF proofs. Your contract’s randomness path runs for real, not mocked. - A minimal casino:
LocalTestToken: a freely mintable six-decimal chUSD stand-in, matching USDC.LocalCasinoHostandLocalCasinoVault: a minimal stand-in for the productionCasinoGameFacetthat runs the fullICasinoGameV2session lifecycle (open, randomness or player actions, settle, escrow deltas, payout caps, forfeit and cancel-stuck-randomness) and emits byte-identical events. No diamond and no portfolio risk accounting. It keeps the diamond’s game registry: the slot title deployer, the graph title deployer and a casino bytecode deployer are registered on it, and dev account #0 is its security council. It checks no RTP band.CoinflipGame: the reference coinflip contract, listed after your game in the game picker. When your game registers under the nameCoinflipGametoo, the picker lists only yours.- Each game in
/__local-contracts.jsoncarries the Game ID it took when it registered (ADR 0025), numbered as a fresh deployment of the diamond numbers them.
- Your game: the contract, slot title or graph title
deployment.tsnames, compiled and created through the registered deployer of its kind, from dev account #0, which the creation call names as its game developer. A title at or below the 98% approval ceiling registers whitelisted. A title above it, and every raw-SDK casino game, registers as a pending game: the terminal says so, and the simulator’s council account approves it so you can play it. On a live chain it takes no sessions until the security council approves it. The game is listed first in the game picker. - Funding: player tokens and vault liquidity. The host’s router balance starts empty: every
call that may request randomness pays for it, as on chain. The simulator page serves the
deployment at
/__local-contracts.json.
Your game contract
Section titled “Your game contract”deployment.ts in the project’s root names the contract:
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default { kind: 'casino', contract: { source: 'src/MyGame.sol', name: 'MyGame' },} satisfies DeploymentInput;contract.sourceis relative tocontracts/. Import the interface as@chain-protocol/casino-sdk/contracts/ICasinoGameV2.sol; it resolves to the Solidity of the SDK your project installed. Other imports resolve relative to the file or fromnode_modules.constructorArgslists constructor arguments.{{diamond}}and{{token}}stand for the addresses ofLocalCasinoHostandLocalTestToken.- For a slot title, write
{ kind: 'slot', title: 'my-slot/title.json' }instead. See Slot Engine. - For a graph-engine title, write
{ kind: 'graph', name: 'My Title', title: 'title.ts' }. See Graph titles. - For a slot threshold Game written in TypeScript, write
{ kind: 'threshold', name: 'DiceGame', title: 'title.ts' }. The module’s default export lists itsThresholdConfigurations by index. See Slot Engine. - Keep
deployment.tsself-contained: import only types.
Graph titles
Section titled “Graph titles”The graph kind deploys a graph-engine title:
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default { kind: 'graph', name: 'My Title', title: 'title.ts', steering: { source: 'MySteering.sol', name: 'MySteering' },} satisfies DeploymentInput;titlenames a module whose default export is theGraphGameDefinition. It may import other modules. The simulator compiles it in a fresh Node or Bun process (Node 22.18 or later, which strips TypeScript types by default).steeringis optional. It names the title’s Steering contract, withsourcerelative tocontracts/. Its constructor takes no arguments. Import the interface as@chain-protocol/casino-sdk/graph-engine/contracts/IGraphSteering.sol.- The simulator deploys the Steering contract, uploads the chunks, creates the title, derives it to
the end (in batches when one 14M-gas call cannot hold it) and registers it under
name. Every bet configuration’s RTP must be below 100%. - A session picks its bet configuration with its game data. Empty game data opens configuration
0, and
0x00 ‖ uint16 configuration ‖ tailopens that configuration, with the 16-bit index big-endian. The engine ignores the tail, which can carry display-only choices such as a dice side.encodePlainGameData(configuration, tail)from@chain-protocol/casino-sdk/graph-enginewrites it. Other leading bytes revertGraphEngine__InvalidGameData. - A change to the title, any module it imports (under Node), or the Steering contract redeploys
the title. The simulator watches the title’s whole import graph, workspace packages outside the
project included, and skips only installed packages under
node_modules. A large title takes many transactions. See Graph Engine → Deploy cost.
The simulator compiles with solc and watches deployment.ts and every file the build read. Save a
change and it redeploys the game at a fresh address within a couple of seconds, with no restart. A
compile error prints in the terminal, and the previous deployment keeps running.
To test a contract built some other way, create it with your own toolchain against
http://127.0.0.1:8545 through the casinoBytecodeDeployer that /__local-contracts.json names,
naming its game developer, and paste its address into the setup panel. The page approves the pending game as the host’s
council and names it under the configured name. A contract no registered deployer created takes no
sessions, as on chain.
Host-path tests
Section titled “Host-path tests”@chain-protocol/casino-sdk/local-host runs the simulator’s host in your own tests, on Hardhat’s
EDR or any dev chain, so a test can open a session, deliver randomness and check the escrow, the
reserve and the payout transfer:
import { deployLocalCasinoHost, localCasinoHostAbi, testRandomnessRouterAbi, whitelistOnLocalHost,} from '@chain-protocol/casino-sdk/local-host';
const { host, vault, token, router } = await deployLocalCasinoHost({ publicClient, walletClient, vaultLiquidity: 10n ** 12n,});await whitelistOnLocalHost({ publicClient, walletClient, host, games: [game] });walletClient’s account deploys the contracts and governs the host as its security council.- Mint the test token to the player and approve the host, then call
openSessionon the host. TestRandomnessRouter.fulfill(requestId, word)delivers the word your test picks, under the router’s 1M callback gas.requestIdis in the host’sCasinoSessionAdvancedevent, anddecodeCasinoSessionreads the session it carries.- The subpath ships ABIs and bytecode, not Solidity. The host follows the production facet’s session lifecycle and changes with it.
What the simulator page replicates from production
Section titled “What the simulator page replicates from production”The host side behaves like the real Chain.wtf host, so your game faces the real integration quirks before it ever ships:
- Optimistic sessions — a
pending:<uuid>row at phaseWAITING_RANDOMNESSis pushed the momentopenSessionis called, then confirmed to the real session id via the broadcast tx hash (both arrival orders handled, exactly like production). - Two live data layers — every chain event reaches the host twice: near-instantly on a
flashblock push channel (in production the host observes flashblocks — sub-block
preconfirmations — merged through a forward-only patch layer) and after a configurable lag on
the indexed session feed (including the intermediate phase-
NONEupdate right afterCasinoSessionOpened). - Monotonic settled results — the settled flip is atomic (payout + gameState together), and a published result never regresses.
- Game-steered balance — the displayed balance follows
betPlaced/revealOutcomewith a delayed chain reconcile, never a raw poll. - Randomness payments —
openSessionandsubmitActionattach the ETH a randomness request owes, the host’srequiredPaymentplus a 10% buffer, starting from an empty router balance. What a request does not use stays in the balance for the next one. A call whose step requests no randomness gets its ETH back in the same transaction. - One request per player call — a step reached from
onRandomnessmay not request randomness again. The host refuses it withLocalCasinoHost__RandomnessRequestedFromCallback, the router logs the failed delivery asRandomnessCallbackFailed, and the session waits forcancelStuckRandomness, which refunds the whole escrow. Derive every further random value from the oneonRandomnessreceived. - No escrow change from a callback — a step reached from
onRandomnessmay not return a nonzeroescrowDelta. The host refuses it withLocalCasinoHost__EscrowChangedFromCallback, and the session waits forcancelStuckRandomnessas above. - Stuck-randomness path — an expired randomness deadline surfaces the permissionless
cancelStuckRandomnessbanner. The VRF node inchain devanswers every request within a second, so a session reaches this path only if the node falls behind.
The setup panel
Section titled “The setup panel”The collapsible sidebar is your chaos-engineering console:
- Flashblock / indexer lag sliders — tune both data layers live. Crank the indexer lag to watch your game ride the optimistic layer; a well-built game shows no flicker and no duplicate results.
- Wallet status override — force
ready/disconnected/setup-requiredto exercise your game’s non-ready screens. - Max-wager answer — choose what
hostApi.getMaxWageranswers, live: alimitat a cap you set in whole tokens (1000 by default),no-limit, orunknown. The game sees a change at its next request. Use it to exercise your bet clamp and your fallback for an unknown answer. - Game URL + contract address — swap between the example and your own game without restarting anything.
The simulator advertises capabilities.playAccess: true. A guest requestPlayAccess() records a simulated play-access intent in the frame and Bridge calls log. Resolution accepts the intent; it does not make the wallet ready, authenticate, sign, fund, or bet. Change readiness explicitly with Wallet status override and wait for the resulting snapshot. An already-ready request is a no-op.
Intentional differences from production
Section titled “Intentional differences from production”- Bets are plain EOA transactions instead of the host’s gasless smart-vault signing flow. The
game-facing API and timing are unchanged;
wallet.addressandwallet.smartVaultAddressare both the local EOA, which also sends the randomness payment. On chain.wtf a relayer sends it, and a covered call charges it to the player in the token. getMaxWageranswers the cap from the setup panel, whatever game data you pass. The production host reads the vault’s maximum wager for your game and that game data. The local host does not enforce the cap: it admits any wager the game’s quotes allow.getRandomnessVerificationruns the production checks against the local Verify Network router (real ECVRF proofs and enclave signatures), so the provably-fair view is testable locally. It is optional and feature-detected, so your game must still handle hosts without it.