# 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 [--out ]` | Rebuilds tier lists and RTP from a live title's chain bytes. |
| `chain slots import-stake --out [--name ] [--seeds-per-prize ]` | 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 --help` lists every
flag.
## Verify a live title
```sh
mkdir decoded
npx chain slots decode --rpc --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": { "": ["", …] } }`,
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.