# Migrating from Stake Engine Bring your Stake Engine slot math to Chain's slot engine and keep your frontend animations. Import the math export, then replace the RGS client with the casino SDK bridge. :::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/) ::: A Stake Engine slot moves to the Chain [slot engine](/sdk/casino/slot-engine/) without redoing its math. The importer converts your math export into a title. Each bet mode becomes a bet configuration, and the lookup table weights become an on-chain settlement table with the same RTP. Your books become what the frontend replays. The frontend keeps its animations; only the RGS client is replaced by the casino SDK host bridge. ## Migrate a game Run every command in your game's project, with `@chain-protocol/casino-sdk` and `@chain-protocol/cli` installed at the same version (see [Slot engine → Build your own title](/sdk/casino/slot-engine/#build-your-own-title)). The commands use `npx chain`; with Bun, write `bunx chain` instead. 1. **Export the math.** Run your game in the Stake math SDK as you would for a Stake upload. The importer reads the upload folder, `games//library/publish_files/`: ```text index.json lookUpTable_base_0.csv books_base.jsonl.zst lookUpTable_bonus_0.csv books_bonus.jsonl.zst ``` 2. **Import it.** ```sh npx chain slots import-stake /games//library/publish_files \ --out my-slot --name "My Slot" ``` Expect one block per mode, with `imported RTP` matching `source RTP`. A mode that never pays 0 (bonus buys, typically) can drift by a few millionths of a percent: ```text base source RTP 96.700000% imported RTP 96.700000% (weights rescaled to 32 bits) books 1348 written bonus source RTP 96.700000% imported RTP 96.699940% (weights rescaled to 32 bits) books 8166 written ``` `my-slot/` now holds `title.json`, a tier list and a display mapping per mode, and `books//.json`. 3. **Check the title.** Expect every mode's RTP inside 85% to 98%, and `0 prizes without seeds` for every mode. ```sh npx chain slots compile my-slot/title.json npx chain slots check-mapping my-slot/title.json ``` 4. **Port the frontend** (next section) and serve `my-slot/books/` from its public folder at `/books/`. 5. **Play it locally.** Point `deployment.ts` at `my-slot/title.json` and run `npx chain dev casino --frontend-url `, as in [Slot engine → Build your own title](/sdk/casino/slot-engine/#build-your-own-title), step 5. Expect `Deployed My Slot at 0x…`. Open the URL it prints and pick your title in the game list. 6. **Deploy and hand off** as in [Slot engine → Build your own title](/sdk/casino/slot-engine/), steps 6 and 7. ## Port the frontend Your Stake frontend talks to the RGS. On Chain it talks to the host through `@chain-protocol/casino-sdk/guest`, and the host signs every transaction. Keep the renderer that plays a book's `events`; replace the RGS client with this flow: ```ts import { connectGameToHost, type HostSnapshot } from '@chain-protocol/casino-sdk/guest'; import { hexToBigInt, toHex, type Hex } from 'viem'; import { parseDisplayMapping, pickDisplaySeed } from '@chain-protocol/casino-sdk/slot-engine'; import baseMapping from './my-slot/base.display-mapping.json'; import bonusMapping from './my-slot/bonus.display-mapping.json'; // Same order as title.json, which follows index.json. const MODES = [ { name: 'base', cost: 1n, mapping: parseDisplayMapping(baseMapping) }, { name: 'bonus', cost: 100n, mapping: parseDisplayMapping(bonusMapping) }, ]; const host = await connectGameToHost({ async setState(snapshot) { onSnapshot(snapshot); }, }).promise; // Replaces POST /wallet/play. The wager is what the player pays: base bet × mode cost. async function play(modeIndex: number, baseBet: bigint) { const { sessionKey } = await host.openSession({ wager: (baseBet * MODES[modeIndex].cost).toString(), gameData: modeIndex === 0 ? '0x' : toHex(modeIndex, { size: 1 }), }); return sessionKey; } // Replaces the play response: when your session settles, load a book with the same payout. async function bookFor(modeIndex: number, gameState: Hex, randomness: Hex) { const mode = MODES[modeIndex]; const bookId = pickDisplaySeed(mode.mapping, hexToBigInt(gameState), randomness); return (await fetch(`/books/${mode.name}/${bookId}.json`)).json(); } function onSnapshot(snapshot: HostSnapshot) { // Find your session in snapshot.sessions.items by sessionKey. Once it is settled and // raw.gameState is 4 bytes, call bookFor(modeIndex, raw.gameState, raw.randomness), play // book.events, then replace POST /wallet/end-round with: // host.revealOutcome({ sessionId: row.sessionId }) } ``` `parseDisplayMapping` and `pickDisplaySeed` come from `@chain-protocol/casino-sdk/slot-engine`. The complete working version of this loop, including in-flight and cancelled sessions, is the Lucky Reels frontend, `packages/casino-sdk/slot-engine/example/frontend/src/App.tsx` in the Chain repository. | Stake RGS | Chain casino SDK | | ----------------------------------------- | ---------------------------------------------------------------------------------------- | | `sessionID`, `rgs_url` URL params | Nothing; the host connects to your iframe. | | `/wallet/authenticate` | The first snapshot: `wallet`, `balances`, `token`. | | `/wallet/balance` | `snapshot.balances.smartVaultBalance`, pushed on every change. | | `/wallet/play` with `mode` and `amount` | `openSession({ wager, gameData })`, with `wager` = base bet × cost. | | The round's book | Settled `gameState` → `pickDisplaySeed` → `/books//.json`. | | `/wallet/end-round` | `revealOutcome({ sessionId })` after your win animation. The payout is already on chain. | | `/bet/event`, resuming an active round | `snapshot.sessions.items` lists the player's sessions, open ones included. | | `minBet`, `maxBet`, `stepBet`, bet levels | `getMaxWager({ gameData })` for the mode's live maximum wager; your own levels. | | Amounts with 6 decimals | Token base units; `snapshot.token.decimals` (6 for USDC and chUSD). | | `lang` URL param | `snapshot.ui.locale`; see [Visual & UX](/sdk/casino/visual-and-ux/). | **Ask the host for the maximum wager.** Chain has no fixed `maxBet` and no minimum wager. The largest wager the vault accepts follows from the mode's settlement table and from the vault's state, so it differs per mode and moves over time: ```ts const gameData = modeIndex === 0 ? '0x' : toHex(modeIndex, { size: 1 }); const answer = await host.getMaxWager({ gameData }); // 'limit' → cap the wager (base bet × cost) at BigInt(answer.maxWager), in token base units // 'no-limit' → the host caps nothing for this mode; the balance is the bound // 'unknown' → fall back to your own limits, never to unlimited ``` Ask again when the player switches mode, and on an interval. See [Asking the host for the maximum wager](/sdk/casino/building-games/#asking-the-host-for-the-maximum-wager). Also add a `game.manifest.json` next to your `index.html`; see [Building Casino Games](/sdk/casino/building-games/). The Stake web SDK's Svelte stack works as-is; any static build the host can load in an iframe does. ## How the conversion works | Stake Engine | Chain slot engine | | -------------------------------- | -------------------------------------------------------------------- | | Bet mode in `index.json` | Bet configuration, same order; the first is the base configuration. | | Mode `cost` | Folded into the wager. Prizes are multiples of the mode's own wager. | | `payoutMultiplier` (hundredths) | Prize = `payoutMultiplier / (100 × cost)`. | | Lookup table rows | Summed by payout into one tier per distinct prize. | | Books | Display seeds: a sample of book ids per prize. | | RTP checked by the RGS on upload | RTP derived on chain by `SlotTitleDeployer` at deploy. | - **Who decides the outcome.** On Stake the RGS draws a book. On Chain the engine draws a prize from the tier list with verifiable randomness, and the frontend then shows a book with exactly that payout. Books never affect odds or payouts. - **Weight rescaling.** Stake weights are `uint64`; the engine's winning weights must sum to at most 2³² − 1. When they don't fit, the importer scales winning weights down, keeps every prize (a rare prize keeps weight 1 at minimum), and solves the miss weight so the RTP matches the source. Probabilities of individual prizes shift by the rounding; the output reports the RTP. - **Which books are kept.** For each prize the importer keeps up to `--seeds-per-prize` book ids (default 16), chosen in proportion to their lookup-table weight. A prize reached by both a base win and a free-spins round is therefore shown in the same mix as on Stake. Raise the count for more variety; lower it to shrink `books/`. Tested on the math SDK's `0_0_lines` sample, the default wrote about 9,500 books (98 MB), each fetched on its own. - **Fractional prizes.** A cost like `3` makes prizes such as `1/3`; tier lists accept fractions, and the compiler picks a prize denominator that holds them exactly. - **Compressed books.** Reading `.jsonl.zst` needs Node 22.15 or newer. On older Node, run `zstd -d` on the books files first; the importer falls back to the uncompressed `.jsonl`. ## What does not carry over - **Stake's RTP rules.** Stake requires 90% to 98% per mode, within 0.5% across modes. Chain requires each configuration to sit in the council's RTP band, takes a title whose highest RTP is above 98% only once the council approves it, and treats the base configuration's RTP as the title RTP. - **Stake's payment terms.** On Chain you are paid the game dev fee: a share of each session's edge, its wager × (1 − the mode's RTP), credited when the session settles whether the player won or lost. The engine quotes each mode from its settlement table, so you write no quote. See [Contract Constraints → Fee split](/sdk/casino/contract-constraints/#fee-split). - **Jurisdiction flags** (`socialCasino`, `disabledTurbo`, and the rest) have no equivalent. - **Criteria and force files** are math SDK tooling and stay there. Keep them in your math repo and rerun the import after each math change.