Skip to content

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.

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.

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.

  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/<game>/library/publish_files/:

    index.json
    lookUpTable_base_0.csv books_base.jsonl.zst
    lookUpTable_bonus_0.csv books_bonus.jsonl.zst
  2. 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 RTP matching source RTP. A mode that never pays 0 (bonus buys, typically) can drift by a few millionths of a percent:

    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/<mode>/<id>.json.

  3. Check the title. Expect every mode’s RTP inside 85% to 98%, and 0 prizes without seeds for every mode.

    Terminal window
    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 <your frontend URL>, as in 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, steps 6 and 7.

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 RGSChain casino SDK
sessionID, rgs_url URL paramsNothing; the host connects to your iframe.
/wallet/authenticateThe first snapshot: wallet, balances, token.
/wallet/balancesnapshot.balances.smartVaultBalance, pushed on every change.
/wallet/play with mode and amountopenSession({ wager, gameData }), with wager = base bet × cost.
The round’s bookSettled gameState → pickDisplaySeed → /books/<mode>/<id>.json.
/wallet/end-roundrevealOutcome({ sessionId }) after your win animation. The payout is already on chain.
/bet/event, resuming an active roundsnapshot.sessions.items lists the player’s sessions, open ones included.
minBet, maxBet, stepBet, bet levelsgetMaxWager({ gameData }) for the mode’s live maximum wager; your own levels.
Amounts with 6 decimalsToken base units; snapshot.token.decimals (6 for USDC and chUSD).
lang URL paramsnapshot.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 unlimited

Ask 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.

Stake EngineChain slot engine
Bet mode in index.jsonBet configuration, same order; the first is the base configuration.
Mode costFolded into the wager. Prizes are multiples of the mode’s own wager.
payoutMultiplier (hundredths)Prize = payoutMultiplier / (100 × cost).
Lookup table rowsSummed by payout into one tier per distinct prize.
BooksDisplay seeds: a sample of book ids per prize.
RTP checked by the RGS on uploadRTP 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.
  • 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.