# 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. :::note[Current SDK checkout] - `@chain-protocol/casino-sdk@0.7.0` This guidance reflects this checkout and may include unreleased changes. [Casino changelog](/sdk/casino/changelog/) ::: 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](#threshold-titles) 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](/sdk/casino/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](/sdk/casino/migrating-from-stake-engine/); it converts your existing math export into a title. ## 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](/sdk/casino/getting-started/)). Otherwise install the same version of both: ```sh npm install @chain-protocol/casino-sdk npm install --save-dev @chain-protocol/cli ``` With Bun: ```sh 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. ```csv 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. ```json { "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. ```sh 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. ```sh npx chain slots check-mapping my-slot/title.json ``` 5. **Play it locally.** Point `deployment.ts` in the project's root at the title: ```ts 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: ```sh 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](/sdk/casino/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. ```sh SLOT_ENGINE_PRIVATE_KEY=0x... npx chain slots deploy my-slot/title.json \ --rpc --deployer ``` 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
` 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 ` together with `--developer `: 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. ## Build the frontend The frontend is a normal casino SDK guest (see [Building Casino Games](/sdk/casino/building-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](#the-example-lucky-reels)) does all three. ## Threshold titles 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: ```json { "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. ## Commands | Command | Does | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `chain slots compile ` | Prints each bet configuration's exact figures. | | `chain slots check-mapping ` | Checks the display mappings cover every prize; exits 1 if not. | | `SLOT_ENGINE_PRIVATE_KEY=0x… chain slots deploy --rpc --deployer
` | Deploys the title through the `SlotTitleDeployer`. | | `chain slots decode --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 casino` | Runs 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. ## Verify a live title ```sh 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. ## Rules the chain enforces - **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](/sdk/casino/slots-risk-and-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](/sdk/casino/contract-constraints/#what-your-quotes-decide). ## Reference ### The example: Lucky Reels 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`: ```sh 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`. ### File formats - **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. ### Measured gas 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.