Integration reference

Rabbit Roll Embed Protocol

How an operator embeds our games. Every game is an iframe that asks the platform for money and outcomes and renders what comes back. One integration covers the whole catalogue: adding a game is adding a manifest, never new platform code.

Version v1 Transport postMessage Games live 6 Amounts integer minor units

01Two integration models

The only question that changes anything: who decides the outcome. Pick per game, declared in its manifest.

ModelWho decides the outcomeWho holds the moneyHosting
rabbit Your provably-fair engine. The game only renders what you send. You Must be same-origin — the frame receives outcomes before the player sees them.
external Our certified RNG, inside the game. You. We never touch a ledger. May be remote over HTTPS.

In both models the platform is the trust boundary and the game frame is untrusted, including when the game is ours. The host checks every inbound message against the manifest origin, clamps stakes to the manifest range before they reach the ledger, and refuses any round the frame did not open in this session.

Registering a game is a manifest, not a code change

Drop a JSON entry and the game appears. No lobby edits, no build step. Every field below is enforced: an id must be lowercase kebab-case, a rabbit-engine game must be same-origin, and a declared edge outside 0–0.10 is rejected outright.

// games.json — one entry per game
{
  "id": "bank",
  "name": "BANK",
  "engine": "external",
  "url": "games/bank/",
  "origin": "self",        // or an absolute https:// origin
  "category": "originals",
  "aspect": "9 / 16",
  "minStakeCents": 10,
  "maxStakeCents": 100000,
  "edge": 0.04           // published house edge
}

02Quick start

Three calls cover a whole game. The SDK keeps balance, currency, tier and limits current, so you never track them yourself.

import { connect } from '/sdk/rr-sdk.js';

// resolves once the platform has sent the session
const rr = await connect('my-game');

// 1. commit a stake — resolves once the money has actually left
const round = await rr.bet({ stakeCents: 100, params: {} });

// 2. (optional) act mid-round, for games with a middle
let state = await rr.act(round.roundId, 'hit');

// 3. end the round. Safe to retry: settling twice returns the first result
const result = await rr.settle(round.roundId, {});
// result.multiplier, result.payoutCents, result.balanceCents

rr.onBalance(cents => paintBalance(cents));  // fires on every move
rr.resize(720);                              // tell the host our height

Mounting on the host side is a frame plus the broker. The host sets the frame's aspect ratio from the manifest, points it at the game, and brokers everything after that.

import { createHost } from './platform/host.js';

const host = createHost({
  frame,          // the <iframe> element
  wallet,         // your ledger: auth / balance / debit / credit
  table,          // your fair engine (rabbit-engine games only)
  accountId,
  serverSeed, clientSeed, sha256,
  onEvent: e => log(e)   // ready | bet | settle | error
});

await host.mount('bank');

03Catalogue

Six games registered today. House edge is measured, not estimated: each figure comes from a headless simulation of the finished build under optimal play, held out of sample.

GameEngineHouse edgeRTP AspectNotes
crashrabbit 1.0%99.0%16 / 10 Cash-out curve.
plinkoexternal 1.0%99.0%4 / 3 Row count via params.
longbowexternal 3.6%96.4%16 / 10 Skill duel.
bankexternal 4.0%96.0%9 / 16 Portrait. Mid-round cash-out.
wickexternal 3.5%96.5%16 / 10 Uniform across all 252 zones. Max win 250x.
paydirtexternal 4.0%96.0%16 / 10 Gated by 23 assertions at 20,000 rounds per tier.

Every figure here is measured against the shipped build

Not modelled, not intended: each one comes from simulating the finished game on seeds held out of calibration. paydirt carries its own maths gate — 23 assertions at 20,000 rounds per tier, checking the edge but also that payout parameters cannot reach back and change which blocks broke, and that a late pick pays like the first one.

wick is uniform by construction rather than by measurement: zones are priced at (1 − margin) / probability, and its multiplier cap sits above the fairest quote the table can make, so no cell is quietly shaved.

04Round lifecycle

The ordering here is not stylistic. Each rule closes a way a game frame could otherwise be made to pay twice.

  1. 01
    The stake leaves the balance before the outcome exists.

    A game that renders a win before the debit landed can be made to pay twice. rr:bet resolves only after the money actually moved.

  2. 02
    The outcome is fixed at rr:round, not at rr:settle.

    When the player stops changes what they are paid. It never changes what happened. For a climbing game, the curve was already decided when the bet was accepted.

  3. 03
    Settling twice is a no-op that returns the first result.

    Retries are safe by construction, so a dropped connection cannot double-credit.

  4. 04
    The frame never sees what it is not entitled to.

    In games with hidden state, the platform holds it and sends only the player's view. A frame holding the shoe would hold the dealer's hole card.

External-engine settlement

For external games the provider reports the multiplier and the platform does the arithmetic on its own ledger — it trusts the number, never the math. A missing or negative multiplier settles at zero.

05Message reference

Every message is { type, v: 1, ...payload }. The platform drops anything whose event.origin does not match the manifest, and anything without v: 1.

TypeDirectionPayloadWhen
rr:readygame → host{ gameId }Once, when the game has booted.
rr:betgame → host{ requestId, stakeCents, params }Player commits a stake.
rr:actgame → host{ requestId, roundId, action }Player acts mid-round.
rr:settlegame → host{ requestId, roundId, choice }Player finishes the round.
rr:resizegame → host{ height }Natural height changed.
rr:inithost → game{ sessionId, currency, balanceCents, tier, limits, edge }After rr:ready.
rr:balancehost → game{ balanceCents }Any time the balance moves.
rr:roundhost → game{ requestId, roundId, outcome, proof }The bet was accepted.
rr:statehost → game{ requestId, roundId, state }The hand after an action.
rr:resulthost → game{ requestId, multiplier, payoutCents, balanceCents }The round settled.
rr:errorhost → game{ requestId, code, message }Anything refused.

params is game-specific and opaque to the platform, except that the engine validates and clamps anything it uses — mine count, plinko rows, dice target. choice is how the player ended it: { cashedAt } for a climbing game, { picked: [...] } for a board game, {} for an instant one.

06Launch URL

For a remote external game the operator opens the launch URL in the frame. Everything the session needs rides on the query string; there is no in-game wallet connect, because the operator already knows the player.

https://games.example.com/bank/
  ?token=<session token>
  &operator=<operator id>
  &currency=EUR        // ISO 4217
  &lang=es
  &mode=real           // real | demo — anything else is demo
  &rgs=https://rgs.operator.com
ParameterRequiredMeaning
tokenreal moneySession token. Authenticates the player against your platform.
operatorreal moneyOperator identifier, for routing and reporting.
currencyOptionalISO 4217. Defaults to USD. Display only — amounts are always minor units.
langOptionalUI language. Defaults to en.
modeOptionalreal or demo. Any other value is treated as demo.
rgsreal moneyRemote Game Server base URL — the wallet and outcome authority.

Lobby bridge

A framed game also posts lifecycle events up to the operator shell, so the lobby can react without polling. Standalone, these are inert.

EventPayloadUse
balanceChanged{ cents }Repaint the lobby balance.
roundStart{ roundId, stakeCents }Session tracking, reality checks.
roundEnd{ roundId, winCents, multiplier, seed }History, big-win feeds.
error{ code, message }Surface a failure in the shell.
goToLobby{}Player asked to exit the game.
requestDeposit{}Player ran out of funds — open your cashier.

07Wallet API

The half you build. Five endpoints on your side; our game server calls them and renders whatever your platform says. This is the entire server contract — there is nothing else to implement.

Direction matters

In this model you are the operator and we are the provider. Your wallet is the authority on the player's money and we never hold a balance. If instead you want us to host the wallet, that is the rabbit engine in section 01 and this chapter is ours to implement, not yours.

Transport

DetailValue
MethodPOST
Base URLYours. You give it to us at onboarding, one per environment.
Paths/auth · /balance · /bet · /win · /rollback
Bodyapplication/json
Identity headerx-operator: rabbitroll
Signature headerx-signature: <hex>
AmountsInteger minor units. 1250 is 12.50, never 12.5.
Timeout we apply6000 ms by default, configurable per operator.

Paths and field names are configurable per operator. If your wallet already speaks txId and amount, say so and we remap on our side — that is a config entry, not a change to your platform and not a fork of ours.

Signing

x-signature is HMAC-SHA256, hex, keyed with the shared secret, over the raw request body exactly as received. Do not parse and re-encode before verifying: key order and whitespace change the bytes, and a re-encode is the single most common reason a correct integration fails its first signed call.

// verify — Node, and the two lines that matter
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest();
  const given = Buffer.from(String(header || ''), 'hex');
  // length check first: timingSafeEqual throws on a mismatch
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// Express: keep the raw bytes, do not let a JSON parser eat them
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

The five verbs

VerbWe sendYou answerMeaning
auth token, currency playerId, balanceCents, currency Resolve the launch token to a player. Called once when the game mounts.
balance playerId, currency balanceCents, currency Read. No side effects, ever.
bet playerId, amountCents,
transactionId, roundId, currency
balanceCents, transactionId The stake leaves the player. Refuse with INSUFFICIENT_FUNDS rather than going negative.
win playerId, amountCents,
transactionId, roundId, currency
balanceCents, transactionId The payout returns. A zero payout is not sent at all.
rollback transactionId, roundId, currency voided: true Reverse a prior transaction by its id. See the four rules below.
// a stake, on the wire
POST /bet
x-operator: rabbitroll
x-signature: 9f2c…

{ "playerId": "p_8842", "amountCents": 250, "currency": "EUR",
  "transactionId": "rr_9c1f4a_bet", "roundId": "rr_9c1f4a" }

// 200 — applied
{ "ok": true, "balanceCents": 9750, "transactionId": "rr_9c1f4a_bet" }

// 402 — refused, and a refusal is a clean answer, not an error
{ "ok": false, "error": "INSUFFICIENT_FUNDS" }

The four rules that decide whether money stays correct

Everything above is shape. These four are behaviour, and every one of them exists because it is a way real integrations lose money.

  1. 01
    The same transactionId is the same move, exactly once.

    We retry. Networks duplicate. A repeated id must return the first result and must not apply again. Answer the replay with the same balanceCents you answered the first time.

  2. 02
    A replay carrying a different amount is a conflict, never a correction.

    Answer 409 TRANSACTION_CONFLICT. If you instead treat it as an update you have built a way to rewrite settled money by resending an id.

  3. 03
    A win may arrive before its bet.

    Accept it. Networks reorder, and the round state is ours, not yours. Do not hold the payout waiting for a stake you have not seen; you are the purse, not the referee.

  4. 04
    Rollback must be safe to send twice, and safe for something you never saw.

    Both answer 200. Rolling back an unknown id succeeds and does nothing; rolling back twice applies once; and after a rollback the original id must not be re-applied. This is what lets a timed-out round reach a settled state instead of retrying forever.

A timeout is not a failure

If your wallet does not answer, we do not know whether you applied it. We do not guess: we send a rollback under the same id, which rule 04 makes safe either way. That is why rule 04 is not optional — without it, a slow response turns into a stake taken with no payout and a player who is right to complain.

08Sandbox & proof

You should not have to discover your wallet's edge cases in production, and neither should we. Both sides are testable before a single real bet.

Test your wallet against our client

We run a harness that drives a real provider client through the failure paths a live integration eventually hits: a call that times out mid-round, a duplicated stake, a payout that overtakes its stake, a double rollback, and an operator that double-charges on purpose. You do not run this and you do not need our repository: give us a staging URL and a throwaway secret, and we run it against your wallet and send you the report. If you would rather run it yourself, ask and we will send you the harness.

# what we run on our side, against the endpoint you give us
MOCK_URL=https://staging.yourcasino.example/wallet \
MOCK_SECRET=$YOUR_TEST_SECRET npm run drills

What it asserts is the balance afterwards, not that the calls returned 200. A wallet that answers cheerfully and leaves the player 2.50 short still fails.

Test your side before we exist

We also ship a mock operator: a wallet that enforces the same four rules and that can be ordered to misbehave, so you can build against something that times out and duplicates on demand rather than against a happy path. Ask for it and we send you a single file with no dependencies; it runs on any machine with Node and talks to nothing outside it.

npm run mock:operator                        # a wallet on :4310

curl -X POST localhost:4310/_control -d '{"timeoutNext":1}'
curl -X POST localhost:4310/_control -d '{"latencyMs":800,"rejectWins":true}'
curl localhost:4310/_state                   # every transaction it has seen

What we run on every change

On our side, one command runs four suites and 73 checks: the wallet contract, an adversarial attack suite, the failure drills, and our client against the mock. Nothing ships red. We will run it in front of you on a call, and the output below is what it prints.

npm run verify

  1/4  wallet contract     39 passed, 0 failed
  2/4  attack suite        15 held,   0 breached
  3/4  failure drills      12 passed, 0 failed
  4/4  adapter check        7 passed, 0 failed

Credentials for the sandbox

Onboarding gives you a provider id, a test secret and a demo-balance wallet, in an environment with its own secret that never touches production. Everything in section 07 behaves identically there; only the authority behind the outcome changes.

09Errors

Refusals arrive as rr:error against the requestId that caused them, so the pending call rejects rather than hanging. The SDK times out after 10 seconds.

CodeMeansGame should
insufficient_fundsBalance below the stake.Offer a lower stake, or emit requestDeposit.
stake_out_of_rangeOutside the manifest min/max.Clamp the stepper to the limits from rr:init.
unknown_gameNo manifest with that id.Fail loudly — a registration problem, not a runtime one.
round_not_foundRound not open in this session.Never act on a round the frame did not open.
already_settledRound already closed.Treat as success and read the first result.
disabledGame switched off.Return the player to the lobby.

10Requirements

Frame

Give the frame the manifest's aspect and let the game drive height through rr:resize when it needs to. Games are built mobile-first; bank is portrait (9:16) and the rest are landscape. Allow autoplay so audio can arm on first gesture.

Money

All amounts are integer minor units — cents, never floats. The platform clamps stakes to the manifest range before the ledger sees them. Credits are keyed by a reference derived from the round, which is what makes retries idempotent.

Origin

origin is not decoration. Every message from a frame is checked against it; without that check, any page the frame navigated to could post a "pay me" message and be believed. Remote origins must be HTTPS. A rabbit-engine game must be same-origin, because it receives outcomes before the player sees them.

Fairness

Every game ships a verification page: commit hash before the round, seeds revealed after, and an independent client-side recomputation of the result. Round history is queryable in the game; the authoritative ledger stays yours.

11Security model

The wallet is where money can be taken, so it is the part written to be attacked rather than merely used. This section states what we guarantee, what we do not, and what your side has to hold up.

What the wallet guarantees

GuaranteeHow it is enforced
A game cannot name a player The account comes from the launch session, never from the request body. A provider that sends someone else's player id is ignored, because the field is not read.
A provider cannot touch another provider's transactions Transaction ids are namespaced per provider. Ids are chosen by you, so across two providers they collide by accident and can be made to collide on purpose. An id you do not own is an id you cannot name.
A refund follows the transaction, not the caller A rollback whose original transaction belongs to another player is refused with TRANSACTION_NOT_YOURS, even inside the same provider.
A signature is checked before anything else Verified over the raw body, in constant time, before the session is resolved and before the ledger is touched. An unsigned or mis-signed call never reaches money.
The same id is the same move, exactly once A replay returns the first result. A replay carrying a different amount is a conflict, never a correction — that is a fraud signal, not a retry.
Amounts are integers or they are refused No rounding, no coercion. 10.5 and "1e9" are rejected rather than interpreted. Negative stakes and negative payouts are rejected.
Sessions expire and are bound to one provider A token seen in a launch URL is useless to anyone else, and useless to its owner after the TTL.
The balance cannot be raced below zero Concurrent stakes against one balance settle one-at-a-time; the second is refused rather than both being applied.

These are tested, not asserted

Every line above corresponds to an attack in an adversarial suite that runs on every change. Each case is written as an attack that must be refused: forged signatures, a signature valid for a different provider, a stolen launch token, negative and fractional and string amounts, replay with an inflated amount, rollback of another provider's transaction, rollback aimed at another player, pre-emptively burning a rival's transaction id, expired sessions, overdraft, an unregistered caller, and a concurrent double-spend. Alongside it runs a failure suite that asserts the player's balance is exact after timeouts, duplicates, reordering and an operator that double-charges on purpose.

Two of these were real

The cross-provider rollback and the cross-player refund were live defects found by writing the attacks rather than by reading the code. Both are fixed and both now have a test that fails if they come back. We would rather tell you that than publish a page claiming the code was correct all along.

What we do not guarantee, and you must

  1. 01
    The frame is untrusted, including ours.

    Validate event.origin on every message and never accept a stake amount, a payout or a result from the iframe. The frame asks; your platform decides. Anything a player can reach with a debugger is a request, not a fact.

  2. 02
    The shared secret is the whole perimeter.

    Anyone holding it can move that provider's money. At least 24 characters, out of source control, rotatable without a deploy, and different per environment. Tell us immediately if a staging secret ever touched production.

  3. 03
    A timeout is not a failure.

    If a wallet call times out, the transaction may or may not have been applied. The only safe recovery is a rollback under the same id, which is defined to be safe for a transaction we never saw and safe to send twice. Treating a timeout as "did not happen" is how a stake gets kept without a payout.

  4. 04
    Reconcile, do not trust.

    Every money move is one ledger entry with a stable reference, so your totals and ours are comparable per round, per game and per day. A mismatch should page someone. Our own suite deliberately includes an operator that double-charges, precisely so that reconciliation is the thing that catches it.

  5. 05
    Rate limits, transport and egress are yours.

    TLS on the wallet endpoint, a rate limit per provider, and an allowlist of our egress addresses if your risk team wants one. We will give you static addresses on request. HMAC proves who signed, not how often they may call.

12Before real money

What exists today is a complete, playable integration on a demo balance. Three things sit between that and taking real bets, and none of them are code we can write alone.

  1. 01
    Certified RNG and a signed RGS.

    For real money the outcome must be computed and signed by a certified server — GLI, iTech Labs or BMM — with the client only playing back the result. Our deterministic simulation and provably-fair seed are the fairness proof and the exact model such a server runs; today it runs client-side in demo.

  2. 02
    Game math certification.

    Paytable, RTP and max win are locked and certified per jurisdiction. The measured figures in the catalogue are ours, not a lab's, and wick and paydirt have none yet.

  3. 03
    Responsible gaming and jurisdiction.

    Session, loss and time limits, reality checks, RTP disclosure and a jurisdiction gate. The session already carries a limits object; enforcement is the operator's.

What you can do today

Mount any game against a demo wallet and exercise the full round lifecycle — bet, act, settle, error paths, retries. The wallet contract you integrate against in demo is the same one real money uses; only the authority behind the outcome changes.