Skip to content

Slot engine

Build one-draw games with settlement tables or threshold rules, without writing Solidity. Compile the math, connect your frontend, and let the shared slot engine settle the bet.

The slot engine is part of the casino SDK. One shared contract settles every slot title. A title is a settlement table per bet configuration, deployed as a clone of the engine. A threshold title holds five numbers per bet configuration instead. You write no Solidity. For a table title you write a tier list and check it with chain slots. Then you build the frontend that shows the result.

Prefer an engine to your own contract. A slot-engine title needs no contract audit. The chain derives its RTP and risk figures 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. If your game has several steps or player choices, use the graph engine. Write your own contract only for a game neither engine can express.

  • @chain-protocol/casino-sdk/slot-engine holds the library (pickDisplaySeed, parseDisplayMapping, the title compiler compileSlotGameFile, the deploy helpers) and the compiled contracts.
  • @chain-protocol/casino-sdk/slot-engine/threshold holds the threshold helpers. It has no Node imports, so a frontend can import it.
  • @chain-protocol/casino-sdk/slot-engine/contracts/* holds the contracts as Solidity source. They import @openzeppelin/contracts, an optional peer dependency; install it only if you compile them yourself.
  • chain slots from @chain-protocol/cli compiles, checks, deploys and decodes table titles with the slot engine of the casino SDK your project installed.

From 0.6.0 the TypeScript API calls a title a Game: compileSlotGameFile, deployTableGame and deployThresholdGame. The old Title names remain as aliases. The Solidity contracts and the deployment.ts field title keep their names.

Coming from Stake Engine? Follow Migrating from Stake Engine; it converts your existing math export into a 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:

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

With Bun:

Terminal window
bun add @chain-protocol/casino-sdk
bun add --dev @chain-protocol/cli

The commands below use npx chain. With Bun, write bunx chain instead.

  1. Write a tier list per bet configuration: prize,weight lines. Prizes are multiples of the session wager, as decimals (72.5) or fractions (1/3); prize 0 carries the miss weight.

    prize,weight
    0,9000
    0.5,900
    72.6,9
    200,1
  2. Describe the title. The first bet configuration is the base configuration; its RTP is the title RTP. prizeDenominator is optional and defaults to the smallest grid that holds every prize. displayMapping is optional.

    {
    "name": "My Slot",
    "betConfigurations": [
    {
    "name": "base",
    "tierList": "base.csv",
    "displayMapping": "base.display-mapping.json"
    },
    { "name": "bonus", "tierList": "bonus.csv" }
    ]
    }
  3. Check the figures. Expect exact RTP as a fraction, hit rate, top prize and chunk count for each bet configuration.

    Terminal window
    npx chain slots compile my-slot/title.json
  4. Check the display mapping. Expect 0 prizes without seeds for each configuration; any uncovered prize exits 1.

    Terminal window
    npx chain slots check-mapping my-slot/title.json
  5. Play it locally. Point deployment.ts in the project’s root at the title:

    import type { DeploymentInput } from '@chain-protocol/casino-sdk';
    export default {
    kind: 'slot',
    title: 'my-slot/title.json',
    } satisfies DeploymentInput;

    Serve your frontend, then start the SDK simulator:

    Terminal window
    npx chain dev casino --frontend-url http://localhost:5173

    Expect Deployed My Slot at 0x… and then Play your game at …. Open that URL, pick My Slot in the game list and spin. Every session settles through the local VRF node against the real engine bytecode. Save a change to the title or its tier lists and the simulator deploys it again. See Local Simulator.

  6. Deploy. The account of SLOT_ENGINE_PRIVATE_KEY uploads the chunks and sends the creation call. Expect the title address and title RTP, then the game developer and the status the title registered with: whitelisted, or pending above the approval ceiling (98% at genesis), until the security council approves it. The command fails if the chain derived different figures than the compiler. The SlotTitleDeployer is part of Chain’s contracts deploy: every contracts deployment records its address as slotTitleDeployer beside the protocol’s other contracts.

    Terminal window
    SLOT_ENGINE_PRIVATE_KEY=0x... npx chain slots deploy my-slot/title.json \
    --rpc <url> --deployer <slotTitleDeployer address>

    The command checks SLOT_ENGINE_PRIVATE_KEY before it compiles; without it, it exits 1. The creation call names the game developer, who collects the game dev fee: --developer <address> names one, and without it the command names the signing account. Anyone may name any developer, since the name only directs the fee. An authorship is an optional way for a developer’s wallet to authorize a specific creator wallet: have that wallet write an authorship naming the title inputs hash and the signing account, and pass its id with --authorship <id> together with --developer <that wallet>: the named developer must be the authorship’s writer.

  7. Submit it through the developer platform with your frontend. The build reads title.json and its tier lists, and compiles no contract. The review covers your frontend only. The platform builds table titles; it does not build threshold titles yet.

The frontend is a normal casino SDK guest (see Building Casino Games). Three things are specific to slot titles:

  • Choose the bet configuration with one byte of gameData: 0x (or 0x00) is the base configuration, 0x01 the second one in title.json, and so on.
  • Read the result from the settled session: its gameState is 4 bytes holding the prize in units of that configuration’s prize denominator. The payout is wager × prizeUnits / prizeDenominator, rounded down. A session still in flight has an empty gameState; only decode 4-byte states.
  • Pick what to show with pickDisplaySeed(mapping, prizeUnits, randomness) from @chain-protocol/casino-sdk/slot-engine. Every client derives the same seed for the same session. The seed is an opaque string your frontend knows how to render, such as reel stops or a book id.

The Lucky Reels example frontend (see the example) does all three.

A threshold configuration lets the player name a target multiplier. It holds five numbers instead of a tier list. This title.json is Limbo, which pays any target from 1.01x to 10,000x at 99% RTP:

{
"name": "Limbo",
"betConfigurations": [
{
"name": "limbo",
"threshold": {
"prizeDenominator": 100,
"totalWeight": "1000000000000000000",
"winNumerator": "99000000000000000000",
"minTarget": 101,
"maxTarget": 1000000
}
}
]
}
  • Fields. Call prizeDenominator D, totalWeight W and winNumerator K. Targets are in prize units over D, so 101 is 1.01x. A session rolls u = randomness mod W and result = floor(K / (u + 1)). It pays its target when result >= target and 0 otherwise.
  • Numbers. Write a field as a number or as a string of decimal digits. Use a string above 2⁵³, because JSON numbers lose precision there. totalWeight must fit 64 bits, winNumerator 128 bits and the others 32 bits.
  • One kind per title. A bet configuration has either tierList or threshold. A title holds only one kind.
  • RTP. Every target’s RTP lies between (K - maxTarget + 1) / (D × W) and K / (D × W). The deployer requires the upper end below 100% and registers both ends with the diamond, which checks them against the council’s RTP band. It also requires D < minTarget <= maxTarget and K >= maxTarget.
  • Game data is exactly 5 bytes, uint8 index ‖ uint32 target. Write it with thresholdGameData(index, target) from @chain-protocol/casino-sdk/slot-engine/threshold, the browser-safe subpath, and read it back with decodeThresholdGameData. Any other length reverts SlotEngine__InvalidGameData, and a target outside [minTarget, maxTarget] reverts SlotEngine__TargetOutOfRange.
  • Game state is 20 bytes, uint32 prizeUnits ‖ uint128 result. prizeUnits is the target on a win and 0 on a miss. result is the rolled multiplier in prize units, so the frontend can show it. decodeThresholdGameState reads it, and rollResult and rollThreshold replay a randomness word.
  • Quote. quoteThreshold(configuration, wager, target) returns what the title’s quoteCaps, quoteRiskParams and onSessionStart reserve give the session, bit for bit: maxEscrowStake, maxReservedProfit, openingReservedProfit, maxPayout, probabilityWad, expectedPayout and bodyVarianceScaled. A threshold session reserves its whole cap when it opens.
  • From a TS module. A Game whose configurations live in TypeScript deploys with { kind: 'threshold', name: 'DiceGame', title: 'title.ts' } in deployment.ts. The module’s default export lists the configurations by index, each field a bigint: export default [dice, dice] satisfies ThresholdConfiguration[], with import type { ThresholdConfiguration } from '@chain-protocol/casino-sdk/slot-engine/threshold'. The simulator watches the module and every module it imports, and deploys the Game again when one changes.
  • Locally, chain dev casino creates slot titles through the simulator’s registered SlotTitleDeployer. The simulator’s game registry checks no RTP band. Limbo at 99% registers pending, and the simulator’s council account approves it.
  • Deploy. Every chain slots command handles table titles only. To deploy a threshold Game on a live chain, call deployThresholdGame({ publicClient, walletClient, deployer, configurations, developer, authorshipId }) from @chain-protocol/casino-sdk/slot-engine. Its title inputs hash, for an authorship, is thresholdGameInputsHash(configurations). The developer platform does not build threshold titles yet.
CommandDoes
chain slots compile <title.json>Prints each bet configuration’s exact figures.
chain slots check-mapping <title.json>Checks the display mappings cover every prize; exits 1 if not.
SLOT_ENGINE_PRIVATE_KEY=0x… chain slots deploy <title.json> --rpc <url> --deployer <address>Deploys the title through the SlotTitleDeployer.
chain slots decode <title address> --rpc <url> [--out <dir>]Rebuilds tier lists and RTP from a live title’s chain bytes.
chain slots import-stake <publish_files> --out <dir> [--name <title>] [--seeds-per-prize <n>]Converts a Stake Engine math export into a title.
chain dev casinoRuns the SDK simulator with the title deployment.ts names.

Run them with npx chain … or bunx chain …. npx chain slots <command> --help lists every flag.

Terminal window
mkdir decoded
npx chain slots decode <title address> --rpc <url> --out decoded

Expect quoted figures match the table for each bet configuration, and one tier list CSV per configuration rebuilt from the chunk bytes. A mismatch exits 1.

  • RTP. SlotTitleDeployer walks every chunk and derives total weight, prize sum, top prize and body variance itself. It rejects any table at or above 100% RTP. It then registers the title on the diamond with the lowest and highest RTP of its bet configurations. The diamond refuses a title outside the council’s RTP band. 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 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. deployTableGame and deployThresholdGame take it as developer and name 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 as authorshipId, and the named developer must be the authorship’s writer.
  • Sizes. Winning weights must sum to at most 2³² − 1; the miss weight can use 64 bits. A prize must fit 32 bits of prize units. A chunk holds at most 3,071 prizes; larger tables span several chunks. A title holds at most 255 bet configurations.
  • Risk quotes are exact from the table: top-prize probability and body variance round up. See Slots Risk & Reserves for what they feed.
  • Fees and maximum wager follow from those quotes, per bet configuration: your game dev fee is a share of each session’s edge, and the largest wager the vault accepts falls as a configuration’s variance rises. See Contract Constraints → What your quotes decide.

Lucky Reels is a complete title: three reels, three rows, five lines, one roll. It lives in the Chain repository at packages/casino-sdk/slot-engine/example/, not in the npm package.

  • game.ts holds the reel strips and paytable. The generator and the frontend share it; the chain never sees it.
  • generate.ts enumerates every reel stop combination into base.csv (the settlement table) and display-mapping.json (reel stops per prize). Win weights are combination counts; the miss weight is set so the table pays exactly 96%.
  • title.json names the bet configuration, pins its prize denominator and points at both files.
  • frontend/ is the guest iframe: it decodes the prize, picks reel stops with pickDisplaySeed, spins to them and reveals the outcome.

To run it from a checkout of the Chain repository, in packages/casino-sdk:

Terminal window
bun install
bun run start:slots

Open http://localhost:3300/?game=http://localhost:3500, pick Lucky Reels in the game list and spin. bun run slots:e2e plays 25 sessions against the running stack and expects 25 sessions settled and matched the reference sampler.

  • Display mapping. { "prizeDenominator": 10, "seeds": { "<prizeUnits>": ["<seed>", …] } }, one per bet configuration. Seed counts are not odds and never feed a tier list; the tier list alone decides what a session pays.
  • Chunk. A data contract the title’s own deployer created with deployChunk: one STOP byte, then 8-byte tiers of uint32 prizeUnits ‖ uint32 cumulativeWeight, prizes strictly ascending.

Two-chunk table of 4,534 prizes, production casino facet with the Verify Network router, steady state: about 494k for a miss (307k open, 188k settle) and 515k for a hit. The first round of a title costs about 630k while the facet initialises its per-game risk storage. Deployment is about 7.9M for two chunks plus 1.4M for the title.