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.
A Stake Engine slot moves to the Chain 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
Section titled “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). The commands use
npx chain; with Bun, write bunx chain instead.
-
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/<game>/library/publish_files/:index.jsonlookUpTable_base_0.csv books_base.jsonl.zstlookUpTable_bonus_0.csv books_bonus.jsonl.zst -
Import it.
Terminal window npx chain slots import-stake <math-sdk>/games/<game>/library/publish_files \--out my-slot --name "My Slot"Expect one block per mode, with
imported RTPmatchingsource RTP. A mode that never pays 0 (bonus buys, typically) can drift by a few millionths of a percent:basesource RTP 96.700000%imported RTP 96.700000% (weights rescaled to 32 bits)books 1348 writtenbonussource RTP 96.700000%imported RTP 96.699940% (weights rescaled to 32 bits)books 8166 writtenmy-slot/now holdstitle.json, a tier list and a display mapping per mode, andbooks/<mode>/<id>.json. -
Check the title. Expect every mode’s RTP inside 85% to 98%, and
0 prizes without seedsfor every mode.Terminal window npx chain slots compile my-slot/title.jsonnpx chain slots check-mapping my-slot/title.json -
Port the frontend (next section) and serve
my-slot/books/from its public folder at/books/. -
Play it locally. Point
deployment.tsatmy-slot/title.jsonand runnpx chain dev casino --frontend-url <your frontend URL>, as in Slot engine → Build your own title, step 5. ExpectDeployed My Slot at 0x…. Open the URL it prints and pick your title in the game list. -
Deploy and hand off as in Slot engine → Build your own title, steps 6 and 7.
Port the frontend
Section titled “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:
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/<mode>/<id>.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. |
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:
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 unlimitedAsk again when the player switches mode, and on an interval. See Asking the host for the maximum wager.
Also add a game.manifest.json next to your index.html; see
Building Casino 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
Section titled “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-prizebook 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 shrinkbooks/. Tested on the math SDK’s0_0_linessample, the default wrote about 9,500 books (98 MB), each fetched on its own. - Fractional prizes. A cost like
3makes prizes such as1/3; tier lists accept fractions, and the compiler picks a prize denominator that holds them exactly. - Compressed books. Reading
.jsonl.zstneeds Node 22.15 or newer. On older Node, runzstd -don the books files first; the importer falls back to the uncompressed.jsonl.
What does not carry over
Section titled “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.
- 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.