Guide

Overview & architecture Components Random Number Generator Build a game, step by step 0. Design the math 1. Scaffold 2. Backend (PHP module) 3. Frontend (PixiJS) 4. Verify & test 5. Release Sandbox mode (preview) Creating a sandbox game Distributed dev with MCP Filament admin Exporting to production Multi-step games Game replay Dice Duel as a template Building with AI agents (MCP)

API reference

Conventions Get wallet token load_game play get_accountsum ack_game_session round outcome Game launch URL History & replay Health check

SDK reference

Server SDK (PHP) Client SDK (JS) Hard rules

Game tutorials

Blackjack Game variant API reference Integration guide

Multiplayer

Multiplayer Blackjack Tables API Game flow Real-time updates

Game Development Guide

Build an HTML5 casino game for this RGS with one PHP class (the outcome engine) and one PixiJS frontend (the renderer). The platform handles sessions, wallet debits/credits, round persistence, player history and replay — your code only decides outcomes and draws them. The whole workflow is also exposed as an MCP server so AI agents can build games end-to-end.

Overview & architecture

The RGS (Remote Gaming Server) sits between the game and the casino's wallet. A game has two halves:

  • a frontend (HTML/JS) — what the player sees and interacts with
  • a backend outcome engine — a PHP class the RGS loads, which decides whether the player won and how much
Player's Browser
🌐 Frontend
PixiJS Game
rgs-sdk.js
GameMenu.js
POST /api/rgs
▶
◀
JSON response
RGS Server
⚡ RGS Core
RgsController
load_game →
play →
Your Game Class
GameInterface
bet + win
▶
◀
balance
Casino Platform
💰 Wallet
Platform API
QTech API
Local (dev)
Browser (PixiJS + SDK)
RGS (Laravel + Game Logic)
Wallet (External API)

One round of play

  1. The casino launches your game URL with a token (wallet session) and game_id.
  2. The client SDK calls load_game — the RGS validates the token against the wallet, opens a game session and returns bet configuration + balance.
  3. The player acts; the client SDK calls play with bet_amount + your game-specific parameters.
  4. The RGS runs your game class to decide the outcome, settles bet/win with the wallet (atomically where supported), persists the round for history and replay, and returns your payload plus the new balance and a fresh session.
  5. The client animates the result and calls ack_game_session.

Money is handled in integer cents internally; the wire format uses decimal strings. Game code never touches either — you return a float winAmount and the SDK does the rest.

Components

Server-side Game SDK — app/Sdk/

The abstraction every new game is built on. A game is one stateless PHP class; the SDK handles everything else.

Component Path Role
GameInterface app/Sdk/Contracts/GameInterface.php The contract: play(GameContext): GameResult
GameContext app/Sdk/GameContext.php Input: player, bet, client params, prior state, RNG
GameResult app/Sdk/GameResult.php Output: win amount, client payload, replay data, round state
GameException app/Sdk/GameException.php Clean rejection — aborts before the player is charged
Rng app/Sdk/Rng.php CSPRNG helpers — the only allowed randomness source
GameRegistry app/Sdk/GameRegistry.php Maps game codes → classes (from config/games.php)
GameKernel app/Sdk/GameKernel.php Runs SDK games, persists rounds, adapts to the wire format
GameDispatcher app/Sdk/GameDispatcher.php Routes to the SDK kernel or the frozen legacy engine

Client SDK — resources/rgs-games/_shared/

File Role
rgs-sdk.js The game client API: boot(), play(), ack(), balance tracking, session rotation, replay mode, error normalization
rgs_bootstrap.js Launch-parameter handling (token, rgs_url, replay payloads)
query_string.js Query-string helper

Your rendering layer (PixiJS) sits on top of this — the SDK talks to the server, PixiJS draws the results. See step 3.

RGS API — app/Actions/Rgs/, RgsController

One endpoint, POST /api/rgs, dispatched by the rquest parameter — the full reference is below.

Wallet abstraction — app/Services/Wallet/

Games never touch money. The play pipeline settles every round through a wallet adapter selected by the WALLET_ADAPTER environment variable:

Adapter Use
platform The in-house platform wallet (combined bet+win call)
qtech QTech Game Provider API v1.11 (see docs/QTECH-INTEGRATION.md)
local Dev-only in-process wallet for browser-testing games (refuses production)

Data model

Table Meaning
games Game catalogue: code (game_id), name, min/max bet, RTP, active flag
game_sessions One per round-in-progress per player; rotates when a round completes
game_rounds One per round: bet/win in cents, win type, input/result/steps/state JSON — powers history and replay
rgs_players, rgs_wallet_transactions External-player mapping, wallet audit + rollback data

Tooling

Tool Purpose
php artisan make:game <code> "<Name>" Scaffolds a complete runnable game (backend, frontend, blade view, registry entry, DB row)
MCP server (php artisan mcp:start game-sdk) Lets AI agents build games: docs, reference code, scaffolding, validation, real play-testing, test runs
Reference game Dice Duel (dd) app/Games/DiceDuelGame.php + resources/rgs-games/dd/ — the template to imitate
Test suite Pest; game-test pattern in tests/Feature/Sdk/GameSdkTest.php

Random Number Generator (RNG)

The RGS includes a certified-random module that all games must use for outcome determination. The App\Sdk\Rng class wraps PHP's cryptographically secure random number generator (random_int) and provides game-specific helpers.

Important: Games must never use mt_rand(), rand(), or PHP's shuffle() directly. Always use $context->rng so the RNG source stays swappable and auditable.

Accessing the RNG

The RNG is available in your game's play() method via the context object:

public function play(GameContext $context): array
{
    $rng = $context->rng;

    // Roll a die (1-6)
    $roll = $rng->int(1, 6);

    // Draw 20 unique numbers from 1-80 (keno)
    $drawn = $rng->uniqueInts(1, 80, 20);

    // Shuffle a deck of cards
    $deck = $rng->deck();
}

RNG Methods

Method Description Example
int(min, max) Random integer between min and max (inclusive) $rng->int(1, 6) → 1–6
chance(probability) Returns true with given probability (0.0–1.0) $rng->chance(0.25) → 25% true
pick(array) One random element from a non-empty array $rng->pick(['a','b','c'])
uniqueInts(min, max, count) Draw count distinct integers (lottery/keno style) $rng->uniqueInts(1, 80, 20)
shuffle(array) Cryptographically shuffled copy (Fisher-Yates) $rng->shuffle($items)
weightedPick(weights) Pick a key based on relative weights $rng->weightedPick(['cherry'=>50, 'seven'=>5])
deck() Shuffled 52-card deck as 0–51 indexes $rng->deck()

Card deck encoding

The deck() method returns card indexes 0–51. To decode:

$cardIndex = 27; // Example card
$suit = intdiv($cardIndex, 13);  // 0=spades, 1=hearts, 2=diamonds, 3=clubs
$rank = $cardIndex % 13;         // 0=2, 1=3, ... 8=10, 9=J, 10=Q, 11=K, 12=A

Weighted picks for slots/symbols

Use weightedPick() for slot reels or any outcome where symbols have different probabilities:

$symbol = $rng->weightedPick([
    'cherry'  => 50,  // 50/56 ≈ 89.3%
    'seven'   => 5,   // 5/56 ≈ 8.9%
    'wild'    => 1,   // 1/56 ≈ 1.8%
]);

Security & compliance

  • CSPRNG backed — All methods use PHP's random_int(), which sources entropy from the operating system's cryptographic random number generator.
  • Auditable — Centralizing RNG in one class makes it easy to audit, test, and potentially swap for a hardware RNG or third-party certified source.
  • Deterministic testing — For unit tests, you can mock the Rng class to produce predictable sequences.

Build a game, step by step

Step 0 — Design the math first

  • Game mechanics: which inputs the player gives, how the outcome is decided.
  • Math model: paytable and RTP. Compute the theoretical RTP (Σ probability × payout) — you must state it at registration and it should typically be 90–99%.
  • Round shape: single-step (slots, dice, keno) or multi-step (blackjack-style decisions mid-round)?

Step 1 — Scaffold

php artisan make:game hilo "Hi-Lo Cards" --rtp=96.5 --category=table

Creates a working 50/50 placeholder game, registered everywhere:

Artifact Path
Backend game class app/Games/HiloGame.php
Frontend resources/rgs-games/hilo/index.html + js/game.js
Blade view resources/views/rgs-games/hilo.blade.php
Registry entry config/games.php
Database row games table (skip with --no-db)

Never create these files by hand — always scaffold.

Step 2 — Backend: the PHP outcome module

The game backend is one stateless class in app/Games/ implementing App\Sdk\Contracts\GameInterface. The RGS loads it via the registry in config/games.php. The whole game is play():

public function play(GameContext $context): GameResult
{
    // 1. Validate input (untrusted!) — GameException aborts before any charge
    $guess = strtolower((string) $context->requireInput('guess'));
    if (! in_array($guess, ['low', 'high'], true)) {
        throw new GameException('guess must be "low" or "high"');
    }

    // 2. Decide the outcome — ONLY via $context->rng
    $card = $context->rng->int(1, 13);
    $won  = ($guess === 'high') === ($card > 7);
    $win  = $won ? round($context->betAmount * 1.9, 2) : 0.0;

    // 3. Return payload (for the client) + replay data (for history)
    return GameResult::completed($win)
        ->payload(['card' => $card, 'won' => $won])
        ->input(['guess' => $guess])
        ->outcome(['card' => $card, 'won' => $won])
        ->step('draw', ['card' => $card])
        ->summary("Guessed {$guess}, drew {$card}.");
}

Full GameContext/GameResult/Rng reference: Server SDK. Multi-step games return GameResult::awaitingAction($state) instead — see Multi-step games.

Step 3 — Frontend: PixiJS rendering on the client SDK

The frontend lives in resources/rgs-games/{code}/ and is served at /games/{code}/index.html. It has exactly one job: render what the server returns. It must never compute or predict outcomes. All RGS communication goes through _shared/rgs-sdk.js — PixiJS is the rendering layer on top.

Load order in index.html (shared scripts via relative paths, no hardcoded hosts):

<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.x/dist/pixi.min.js"></script>
<script src="../_shared/query_string.js" onerror="this.remove()"></script>
<script src="../_shared/rgs_bootstrap.js"></script>
<script src="../_shared/rgs-sdk.js"></script>
<script src="js/game.js"></script>

The pattern — boot the SDK, build the PixiJS stage when the game config arrives, and animate server results:

var app, stage;

RgsSdk.boot({
    onReady: function (game) {
        // game.minBet / maxBet / betSteps / defaultBet / balance / currency
        app = new PIXI.Application();
        app.init({ resizeTo: window, background: '#0f0f23' }).then(function () {
            document.body.appendChild(app.canvas);
            buildTable(game);            // draw the idle scene + bet UI
        });
    },
    onReplay: function (replay) {
        // Replay mode: render replay.input / replay.result / replay.steps.
        // replay.winamount and replay.betamount are decimals (same as play API).
        renderReplay(replay);
    },
    onError: function (message) { showErrorOverlay(message); }
});

RgsSdk.onBalanceChange(function (balance) { hud.balance.text = balance.toFixed(2); });

function onSpinPressed(bet, guess) {
    RgsSdk.play({ bet_amount: bet, guess: guess })
        .then(function (res) {
            // res = your GameResult payload + winamount, accountsum, new_game_session
            animateOutcome(res, function () { RgsSdk.ack(); });
        })
        .catch(function (err) { showErrorOverlay(err.message); });
}

Frontend requirements:

  • Build the bet UI from bet_steps / min_bet / max_bet / default_bet — never hardcode bets. If the wallet caps bets, the ladder arrives pre-clamped.
  • Handle the error callback and rejected play() promises: show a message, halt.
  • Support replay mode (onReplay) rendering purely from the persisted round data.
  • The displayed balance always comes from RGS responses (onBalanceChange), never from local arithmetic.
  • Keep everything self-contained under the game folder (assets, sounds); load shared scripts relatively (../_shared/…). Vendor the PixiJS bundle into the game folder for production builds if offline serving is required.

Step 4 — Verify & test

  1. Wiring — registry entry, DB row and files consistent? (The MCP validate-game tool checks this automatically.)
  2. Browser — set WALLET_ADAPTER=local in .env, then open /games/hilo/index.html?token=dev&game_id=hilo. Every token is a separate player starting with 1000.00 EUR. Play wins, losses, insufficient balance, and try to break it.
  3. Automated — write a Pest feature test per the pattern in tests/Feature/Sdk/GameSdkTest.php: full round via POST /api/rgs, payout amount on a win, invalid input → HTTP 400 with no wallet transaction. Run ./vendor/bin/pest.
  4. Replay — open /player/history?token=…, replay a round, confirm it renders from stored data.
it('plays a round', function () {
    $load = $this->postJson('/api/rgs', [
        'rquest' => 'load_game', 'token' => 't', 'game_id' => 'hilo',
    ])->json();

    $res = $this->postJson('/api/rgs', [
        'rquest' => 'play',
        'token' => 't',
        'game_session' => $load['game_session'],
        'bet_amount' => 1.0,
        'guess' => 'high',
    ]);

    $res->assertOk()->assertJsonPath('status', 'OK');
});

Step 5 — Release

  • Confirm the games row (name, min/max bet, RTP, category, is_active).
  • ./vendor/bin/pint on changed files; full test suite green.
  • Deploy — games are served via the public/games symlink (php artisan games:link in the deploy pipeline).

Sandbox mode (preview development)

The sandbox environment lets you develop and test games without wallet/platform registration. Games run with an isolated sandbox wallet (1000.00 EUR balance).

Creating a sandbox game

php artisan make:game hilo "Hi-Lo Cards" --sandbox --rtp=96.5 --category=table

This creates:

  • app/Games/Sandbox/HiloGame.php — backend class
  • resources/rgs-games/_sandbox/hilo/ — frontend files
  • resources/views/rgs-games/_sandbox/hilo.blade.php — blade view
  • Row in sandbox_games table (status: pending)

Test immediately at /sandbox/games/hilo/play.html — no token needed.

Distributed development with MCP

The RGS MCP server supports a distributed workflow where different AI agents or developers work on backend and frontend separately:

┌─────────────────────┐     ┌─────────────────────┐     ┌─────────────┐
│  RGS MCP Server     │     │  Local AI / Dev     │     │  Developer  │
│  (Backend)          │     │  (Frontend)         │     │  (Human)    │
└──────────┬──────────┘     └──────────┬──────────┘     └──────┬──────┘
           │                           │                       │
  1. scaffold-game ────────────────────┼───────────────────────┤
     --sandbox                         │                       │
           │                           │                       │
           │                  2. Build frontend                │
           │                     (HTML/JS/CSS)                 │
           │                           │                       │
           │◄──────────────────────────┤                       │
           │   3. upload-game-frontend │                       │
           │      (base64 ZIP)         │                       │
           │                           │                       │
  4. Preview URL ──────────────────────┼──────────────────────►│
     /sandbox/games/{code}/play.html   │                       │
           │                           │                       │
           │                           │              5. Test & Approve
           │                           │                       │
  6. export-sandbox-game ◄─────────────┼───────────────────────┤
           │                           │                       │

HTTP MCP Endpoint: POST /mcp/game-sdk

# Upload frontend (base64-encoded ZIP)
curl -X POST "https://rgs.cloudvlt.com/mcp/game-sdk" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "upload-game-frontend",
      "arguments": {
        "code": "mygame",
        "zip_base64": "UEsDBBQ..."
      }
    }
  }'

Frontend ZIP requirements:

  • Must contain index.html at root level
  • Should contain js/game.js for game logic
  • Use relative paths: ../_shared/sandbox_bootstrap.js and ../_shared/rgs-sdk.js

Filament admin — sandbox game management

The RGS Filament admin panel (/admin/sandbox-games) provides a UI for managing sandbox games with an approval workflow:

  • Preview — Opens the game in a new tab for testing
  • Approve — Marks game as approved
  • Reject — Marks game as rejected with a reason
  • Export ZIP — Downloads production-ready bundle
  • Delete — Removes game files and database record
Permission Description
view_sandbox_games View the sandbox games list
approve_sandbox_games Approve, reject, or reset game status
delete_sandbox_games Delete sandbox games

Exporting to production

When ready for production, export the game:

php artisan sandbox:export hilo
# Or via MCP: export-sandbox-game code=hilo

This creates a ZIP bundle with:

  • Production-namespaced PHP class
  • Frontend files with production paths
  • config-entry.txt — line to add to config/games.php
  • migration.sql — INSERT for games table

Multi-step games

Games where the player makes decisions mid-round (blackjack: hit / stand / double) keep the round open by returning awaitingAction:

public function play(GameContext $context): GameResult
{
    if (! $context->isContinuation) {
        // First call: the bet is taken now.
        $deck = $context->rng->deck();
        $hand = [array_shift($deck), array_shift($deck)];

        return GameResult::awaitingAction([
                'deck' => $deck,
                'hand' => $hand,
                'bet'  => $context->betAmount, // remember the stake yourself
            ])
            ->payload(['hand' => $hand, 'actions' => ['hit', 'stand']])
            ->step('deal', ['hand' => $hand]);
    }

    // Continuation: betAmount is 0, your saved state comes back.
    $state = $context->state;

    return match ($context->action()) {
        'hit'   => $this->hit($context, $state),
        'stand' => $this->stand($context, $state),  // -> GameResult::completed(...)
        default => throw new GameException('Illegal action'),
    };
}

What the SDK does for you:

  • The session is marked resumable; the next play call routes back with isContinuation = true and your $state.
  • The stake is charged once, on the opening call; the win is paid when you return completed.
  • One round row is kept and updated each step — carry steps forward with ->steps([...$context->steps, ...$newSteps]).
  • The client keeps the same game_session until it receives new_game_session (automatic with the client SDK).

Game replay

Every completed round can be re-watched: the game frontend is re-opened in a special replay mode and re-renders the round purely from the data persisted when it was played — no game logic runs again, no wallet is touched, nothing is random. Replay powers the player history page, admin round inspection, and the QTech game-result verification endpoint, so recording good replay data is a hard requirement, not a nicety.

1. Recording — what the backend persists

During play(), your game records the round on the GameResult:

Call Persisted as What to put in it
->input(array) game_rounds.input What the player chose (their guess, picked numbers, actions)
->outcome(array) game_rounds.result What happened (dice, cards, totals, won/lost)
->step(event, data) game_rounds.steps One entry per visual moment (deal, draw, spin, hit) — the animation script

Design test: could someone re-render the round with only this data, without running your game class? If not, record more. Multi-step games must carry steps forward with ->steps([...$context->steps, ...$new]) so the whole round replays, not just the last action.

2. Delivery — how a replay reaches the game

  1. GET /player/history?token=… lists the player's rounds; each row carries a temporary signed replay URL (2 hours; QTech game-result links are valid 24 hours). Replay URLs cannot be constructed by hand.
  2. The signed URL opens GET /player/replay/{roundId}, which loads the stored round and embeds the game frontend, launching it with replay=<roundId> plus a #replay_data=<base64url-JSON> fragment containing the full round payload.
  3. The same payload is also available raw (with a valid signature) at GET /api/game-round/{roundId}/replay.

3. Rendering — the onReplay callback

The client SDK detects the replay fragment during RgsSdk.boot(), decodes it, skips the load_game handshake and fires onReplay(replay) instead of onReady. The replay object:

Field Meaning
replay.input What you stored via GameResult::input()
replay.result What you stored via GameResult::outcome()
replay.steps The recorded step list, in order
replay.winamount Win amount as decimal (same format as play API)
replay.betamount Bet amount as decimal

How Dice Duel renders it (resources/rgs-games/dd/js/game.js) — show a replay badge, disable the controls, hide the live balance (there is none), and run the normal outcome animation from the stored result:

function runReplay(replay) {
    el.replayBadge.style.display = 'block';
    setBusy(true);                                   // no betting in replay

    var result = replay.result || {};
    var winAmount = replay.winamount || 0;

    el.balance.parentNode.style.visibility = 'hidden';
    if (result.dice) {
        animateRoll(result.dice, function () {
            showOutcome(!!result.won, winAmount, result.total);
        });
    }
}

Behaviour in replay mode:

  • RgsSdk.isReplay() returns true; RgsSdk.play() rejects with "Cannot play in replay mode" — the mode is read-only by construction.
  • No RGS calls are made: there is no session, no live balance, no ack(). Hide or freeze the bet UI and balance HUD.
  • Reuse your normal rendering/animation path fed from replay.result / replay.steps — a PixiJS frontend replays by running the same timeline it uses for live results, just sourced from the stored steps instead of a play() response.

4. Verifying replay (definition of done)

  • Play a few rounds against the local wallet, open /player/history?token=dev, and replay one — it must render correctly from stored data alone.
  • For multi-step games, replay a round with several actions and confirm every step re-renders in order.
  • Endpoint details: History & replay API; launch parameters: Game launch URL.

Using Dice Duel as a template

Dice Duel (code dd) is the reference implementation every new game should imitate — a complete, shippable game in two small files:

Half File What to copy from it
Backend app/Games/DiceDuelGame.php Input validation with GameException, RNG usage, payout math, payload/input/outcome/step/summary recording
Frontend resources/rgs-games/dd/index.html + js/game.js The complete client game loop in ~150 lines: boot → render config → play → animate → ack → repeat, plus replay mode and error handling

The game itself: the player bets low or high, two dice are rolled; totals 2–6 are low, 8–12 are high, 7 pushes (stake returned). A correct guess pays 2.3×.

Workflow: template → your game

  1. Read both Dice Duel files (or use the MCP read-reference-game tool).
  2. Scaffold your game: php artisan make:game <code> "<Name>" — the scaffold is itself a minimal Dice-Duel-shaped game.
  3. Replace the outcome logic in play() keeping the same structure: validate → RNG → payout → record.
  4. Replace the DOM rendering in js/game.js with your PixiJS stage, keeping the SDK calls (boot/play/ack/onBalanceChange/onReplay) exactly as the template uses them.
  5. Try it live: /games/dd/index.html?token=dev&game_id=dd (requires WALLET_ADAPTER=local).

Building with AI agents (MCP)

The repo ships an MCP (Model Context Protocol) server exposing the whole game-building workflow as tools, so an AI agent can scaffold, implement, validate and play-test a game end-to-end without human plumbing.

Connect via HTTP (Remote / Cloud)

The MCP server is available via HTTP at:

https://rgs.cloudvlt.com/mcp/game-sdk

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "rgs-game-sdk": {
      "url": "https://rgs.cloudvlt.com/mcp/game-sdk"
    }
  }
}

Restart Claude Desktop after editing.

Cursor / Windsurf

Add to .cursor/mcp.json or .windsurf/mcp.json in your project:

{
  "mcpServers": {
    "rgs-game-sdk": {
      "url": "https://rgs.cloudvlt.com/mcp/game-sdk"
    }
  }
}

Test with curl

curl -X POST https://rgs.cloudvlt.com/mcp/game-sdk \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Connect via stdio (Local Development)

For local development, the MCP server is auto-wired for Claude Code via .mcp.json:

{
  "mcpServers": {
    "rgs-game-sdk": {
      "command": "php",
      "args": ["artisan", "mcp:start", "game-sdk-local"]
    }
  }
}
Tool Purpose
read-docs This guide, the SDK reference, QTech docs, READMEs
read-reference-game Dice Duel source, scaffold stubs, the client SDK
scaffold-game Runs make:game
list-games All games with engine, limits, frontend status
validate-game Checks registry/class/DB/frontend wiring
play-test-round Plays real rounds through the full pipeline (dev wallet) — verifies outcomes, payouts, input validation
run-tests Runs the Pest suite

The agent workflow

  1. read-docs (topic game-development, then game-sdk) — learn components, process, requirements.
  2. read-reference-game — study Dice Duel (backend + frontend) before writing code.
  3. scaffold-game — generate the runnable skeleton. Never create the files by hand.
  4. Edit app/Games/{Class}Game.php (PHP outcome module) and resources/rgs-games/{code}/js/game.js + index.html (PixiJS UI).
  5. validate-game — verify all wiring is consistent.
  6. play-test-round — play real rounds, including invalid input (it must fail cleanly with no charge).
  7. run-tests — the whole suite must pass; add Pest tests per tests/Feature/Sdk/GameSdkTest.php.

Server implementation: app/Mcp/GameSdkServer.php. Start manually with php artisan mcp:start game-sdk for any MCP-compatible client.

API reference — POST /api/rgs

One endpoint; the rquest parameter selects the method. (POST /api/api.php is a legacy alias.) The client SDK wraps all of this — game code normally never calls these directly.

📦 Postman Collection: Download RGS API Collection — Import into Postman to test all endpoints against https://rgs.cloudvlt.com. Includes load_game, play (Dice Duel + Blackjack), get_accountsum, and ack_game_session with auto-saved session variables.

🧪 API Tester POST https://rgs.cloudvlt.com/api/rgs

Conventions

  • Requests: POST with application/x-www-form-urlencoded or JSON bodies.
  • Responses: JSON. Every success carries "status": "OK"; every failure is HTTP 400 with {"status": "Failed", "msg": "reason"}.
  • Money: decimal strings with 2 decimals on the wire ("accountsum": "998.70"); bets sent as decimal numbers. Internally integer cents.
  • Auth: the wallet session token from the launch URL authenticates every call.
  • CORS: /api/rgs answers with Access-Control-Allow-Origin: *.

Common parameters for every method:

Param Type Required Description
rquest string yes Method name: load_game, play, get_accountsum, ack_game_session
token string yes Wallet session token from the launch URL

auth Get Wallet Session Token

Before calling any RGS API method, you need a wallet session token. This token authenticates the player and is obtained by logging in via the platform API.

POST {platform_url}/api/portal/players/login
Content-Type: application/json

{
  "email": "player@example.com",
  "password": "secret",
  "player_ip": "1.2.3.4",
  "portal_id": 1
}
{
  "status": "success",
  "wallet_session_token": "1|abc123xyz...",
  "player": {
    "id": 42,
    "email": "player@example.com",
    "name": "John Doe",
    "account": {
      "accountsum": 1000.00,
      "currency": { "iso_code": "EUR", "symbol": "€" }
    }
  }
}

The wallet_session_token is what you pass as token to all RGS API calls. It's a Laravel Sanctum token that identifies the player session.

💡 For development: When using WALLET_ADAPTER=local, any token value works (e.g. token=dev). Each unique token gets a fresh player with 1000.00 EUR balance.

🔑 Get Wallet Session Token

rquest load_game

Validates the token against the wallet, opens a game session, and returns the game configuration. Must be called once before playing.

Param Type Required Description
game_id string yes Game code (dd, …) — must match games.game_id
POST /api/rgs
rquest=load_game&token=<wallet-token>&game_id=dd
{
  "game_id": "dd",
  "sname": "dd",
  "name": "Dice Duel",
  "game_session": "01JZWX3E5H8Q2R9T0V1W2X3Y4Z",
  "player_name": "mrCat",
  "currency": "EUR",
  "language": "en",
  "accountsum": "1000.00",
  "min_bet": 0.10,
  "max_bet": 100.00,
  "bet_steps": [0.10, 0.20, 0.50, 1.00, 2.00, 5.00],
  "default_bet": 0.10,
  "lines": 1,
  "status": "OK"
}
Field Meaning
game_session Session id to pass to every play call. One session = one round.
accountsum Player balance (decimal string)
min_bet / max_bet / bet_steps / default_bet Bet configuration — clients must build their bet UI from these; the ladder arrives pre-clamped if the wallet caps bets
lines Line count for slot-style games (1 otherwise)

Failures: missing/invalid token or unknown game → 400 "Invalid token or game not available".

▶ Try it

rquest play

Plays one round — or one step of a multi-step round. Runs the game's outcome logic, settles bet/win with the wallet, persists the round.

Param Type Required Description
game_session string yes The current session from load_game (or the last new_game_session)
bet_amount decimal yes* Stake for this round. *Ignored (treated as 0) when continuing an open multi-step round.
game_id string no Game code (informational; the session already knows it)
game-specific… any per game Every other parameter is handed to the game class as $context->input. Dice Duel: guess=low|high. Multi-step: action=bet|hit|stand|…
POST /api/rgs
rquest=play&token=<t>&game_session=01JZ…&bet_amount=1.00&guess=high
{
  "dice": [6, 4],
  "total": 10,
  "guess": "high",
  "won": true,
  "winamount": 2.3,
  "status_id": 0,
  "accountsum": "1001.30",
  "multi_state": 0,
  "new_game_session": "01JZWX4A7B8C9D0E1F2G3H4J5K",
  "status": "OK"
}
Field Meaning
game payload Whatever the game returned via GameResult::payload() (here: dice, total, guess, won)
winamount Win for this call (decimal)
accountsum Balance after settlement
multi_state 0 = round finished. 1 = round still open — the player must act again
status_id 0 finished, 3 awaiting player action
new_game_session Present only when the round completed: use it for the next round. While multi_state=1, keep sending the same game_session.

Failures (all 400):

  • invalid/expired session → "Game session not found"
  • game input rejected (GameException) → the game's message — no wallet transaction occurs
  • insufficient funds → wallet error message; rollback semantics guarantee the player is refunded on partial failure
▶ Try it

rquest get_accountsum

Balance refresh (e.g. on idle). Takes only the common parameters.

{ "accountsum": "998.70", "currency": "EUR", "status": "OK" }
▶ Try it

rquest ack_game_session

The client confirms the round result was displayed to the player. Fire-and-forget bookkeeping; failures can be ignored by clients.

Param Type Required
game_session string yes
{ "msg": "game session acknowledged", "status": "OK" }

load_resume_game is deprecated — resuming works through play itself: a resumable session routes the next play call back into the open round.

▶ Try it

GET /api/round/{id}/outcome

Returns a signed URL to view the visual replay of a specific game round. Useful for support, auditing, or sharing round outcomes with players.

Param Type Description
id integer The game round ID (from game_rounds table)
GET /api/round/739/outcome
{
  "round_id": 739,
  "replay_url": "https://rgs.cloudvlt.com/player/replay/739?signature=abc123..."
}

Opening replay_url displays the game with the exact outcome of that round, including all bets, cards dealt, and final result.

Game launch URL

Games are static frontends served at /games/{code}/index.html. The casino/aggregator launches them with query parameters:

Param Required Description
token yes Wallet session token for the player
game_id yes Game code, passed to load_game
rgs_url (alias api_url) no Full RGS endpoint URL; defaults to <origin>/api/rgs
device_id no web | mobile
resume_session no Session id to resume
replay + #replay_data= no Replay mode — the game renders the encoded round instead of playing
https://rgs.cloudvlt.com/games/dd/index.html?token=abc123&game_id=dd&device_id=web

Player history & replay

Everything a game records via GameResult::input() / outcome() / step() powers these automatically. Replay links are temporary signed URLs.

Endpoint Data required Purpose
GET /player/history token (query), optional game, page HTML page listing the player's rounds with replay links (2h signed)
GET /player/replay/{roundId} valid signature HTML page embedding the game in replay mode
GET /api/game-round/{roundId}/replay valid signature Raw replay JSON: id, game_code, betamount, winamount (decimals), input, result, steps, created_at

The #replay_data= fragment handed to the game is base64url-encoded JSON of that payload; the client SDK decodes it and calls the game's onReplay(replay) callback. All money values (winamount, betamount) are decimals, consistent with the play API. The full flow — recording, delivery, rendering — is described in Game replay.

Health check

GET /api/health   → 200 { "status": "ok" }
GET /up           → 200 (framework liveness)

QTech platform inbound endpoints (/api/qtech/game-launch, /api/qtech/game-result) and the outbound platform-wallet contract are documented in docs/API.md and docs/QTECH-INTEGRATION.md.

Server SDK reference (PHP)

Namespace App\Sdk. Your game implements one method:

interface GameInterface
{
    public function play(GameContext $context): GameResult;
}

Game classes are resolved through the Laravel container (constructor injection works) and must be stateless — all round state travels through GameContext/GameResult.

GameContext (input)

Property / method Type Meaning
$context->playerId int Internal player id
$context->gameCode string Your game code ('dd')
$context->sessionId string Current game session ULID
$context->currency string Player currency ('EUR')
$context->betAmount float Stake for this call — 0.0 on continuation steps
$context->input array All client parameters of the play call
$context->input($key, $default) mixed Read one parameter
$context->requireInput($key) mixed Read one parameter or throw GameException
$context->action() string Multi-step action ('bet', 'hit', …), defaults 'bet'
$context->isContinuation bool true when resuming an open round
$context->state ?array State returned from the previous step (null on fresh rounds)
$context->steps array Steps recorded so far this round
$context->rng Rng The only allowed randomness source

GameResult (output)

Factory / setter Meaning
GameResult::completed(float $winAmount = 0) Round finished. Wallet settles; the client gets a fresh session.
GameResult::awaitingAction(array $state, float $winAmount = 0) Round still open (multi-step games)
->payload(array) Client-visible response data (winamount and status_id added automatically)
->input(array) What the player chose — persisted for history/replay
->outcome(array) What happened — persisted for history/replay
->step(string $event, array $data = []) Append one replayable step
->steps(array) Replace all steps (use to carry $context->steps forward)
->summary(string) One human-readable line describing the round
->winType(string) Override the recorded win type (default derived win/push/no-win)

Rng

Backed by PHP's CSPRNG. Available as $context->rng:

Method Example
int($min, $max) die roll: int(1, 6)
chance($probability) chance(0.475) → bool
pick($items) pick(['heads','tails'])
uniqueInts($min, $max, $count) keno draw: uniqueInts(1, 80, 20)
shuffle($items) shuffled copy, Fisher-Yates
weightedPick($weights) slot reels: weightedPick(['cherry'=>50,'seven'=>5])
deck() shuffled 52-card deck as 0–51 indexes

GameException

Throw it (or use requireInput()) to reject a request cleanly. The request aborts before any wallet transaction — the player is never charged — and the client receives HTTP 400, which the client SDK surfaces as a rejected promise.

Client SDK reference (JS)

resources/rgs-games/_shared/rgs-sdk.js (ES5, no dependencies). Exactly one of the three boot callbacks fires.

Function Purpose
RgsSdk.boot(options) Bootstrap + load_game handshake; onReady(game) / onReplay(replay) / onError(message)
RgsSdk.play(params) Play a round/step; auto token, session, game_id, session rotation, balance update. Returns a promise.
RgsSdk.ack() Acknowledge the shown result (fire-and-forget)
RgsSdk.refreshBalance() Re-fetch balance (get_accountsum)
RgsSdk.onBalanceChange(cb) Called with the new balance whenever it changes — bind your HUD once
RgsSdk.getBalance() / getSession() / getGame() Current state accessors
RgsSdk.isReplay() true in replay mode
RgsSdk.request(params) Low-level RGS call (adds token, normalizes errors)

The game object passed to onReady: name, balance, currency, minBet, maxBet, betSteps, defaultBet, session. The replay object passed to onReplay: input, result, steps, winamount (cents).

Hard rules

These keep the RGS certifiable — treat them as non-negotiable requirements:

  1. All randomness through $context->rng. Never rand(), mt_rand(), shuffle(), or array_rand() in game code.
  2. Games never touch money. No wallet calls, no balance math beyond computing winAmount from betAmount.
  3. Stateless game classes. Round state only via awaitingAction($state) → $context->state.
  4. Validate every client input — the client is untrusted. Reject with GameException; it aborts before the player is charged.
  5. Record replayable data (input/outcome/step) for every round.
  6. The frontend never decides outcomes. It renders what the server returns; the displayed balance always comes from the RGS response.
  7. Game code is a string stored in the games.game_id column (single source of truth). The game client must exist at resources/rgs-games/{code}/.
  8. The stated RTP must match the implemented math model. New games must never use the legacy eval engine.

Quick reference

php artisan make:game <code> "<Name>"   # scaffold a new game
./vendor/bin/pest                       # run all tests
WALLET_ADAPTER=local                    # .env — then open:
#   /games/<code>/index.html?token=dev&game_id=<code>
php artisan mcp:start game-sdk          # MCP server for AI agents

Blackjack Frontend Development

Complete guide for building a new blackjack frontend that integrates with the RGS.

🎮 Try the demo: Blackjack Pro — Full PixiJS implementation with login, game menu, and complete gameplay. 📥 Download game-pro.js

Game Variant: American Blackjack

The RGS implements American Blackjack with the following rules:

Rule Implementation
Decks 1 deck (configurable)
Dealer hole card Yes — dealer gets 2 cards upfront (one hidden)
Insurance Available when dealer shows Ace
Peek for blackjack Yes — insurance resolves immediately if dealer has BJ
Double down Allowed on 9, 10, 11 only
Split Allowed on pairs (same card type)
Dealer stands on 17 (hard and soft)
Blackjack payout 3:2 (1.5× bet)
Push Bet returned

What makes it American (vs European)

  • Hole card dealt immediately — Dealer receives both cards at deal time
  • Early insurance resolution — If dealer has blackjack after insurance, it pays immediately
  • No "lose all" on dealer BJ — In European, splits/doubles lose everything if dealer has BJ; here insurance protects

Not implemented

Surrender, re-splitting, double after split, side bets (21+3, Perfect Pairs, etc.)

Blackjack API Reference

1. Initialize session — load_game

POST /api/rgs
rquest=load_game&token={token}&game_id=bj

Response includes game_session, accountsum, min_bet, max_bet, bet_steps.

▶ Try it

2. Deal (new round) — play with action=bet

POST /api/rgs
rquest=play&token={token}&game_session={session}&game_id=bj&bet_amount=10&continue=0&action=bet

3. Player actions — play with continue=1

POST /api/rgs
rquest=play&token={token}&game_session={session}&game_id=bj&continue=1&action={action}
▶ Try it

Available actions

Action Description When available
bet Start new round with bet Beginning of round (continue=0)
hit Draw another card Normal play
stand Keep current hand Normal play
double Double bet, take one card, stand Player score 9-11, first decision
split Split pair into two hands Two cards of same value
insurance Side bet (half initial bet) Dealer shows Ace
hit_hand1 / stand_hand1 Actions for first split hand After split
hit_hand2 / stand_hand2 Actions for second split hand After split

Response fields

Field Type Description
status_id int 0 = round complete, 3 = waiting for action
actions string Comma-separated available actions
dealercards string Comma-separated card indices (0-51)
playercards string Player cards (normal play)
player_hand1 / player_hand2 string Split hand cards
dealer_value int Dealer hand value
player_value int Player hand value
result string Outcome text (e.g., player wins, dealer busted)
winamount float Win amount (decimal)
accountsum float Updated balance
new_game_session string New session for next round (when complete)

Card mapping (0-51)

Cards are integers 0-51 representing a standard deck. The API returns card IDs in dealercards and playercards fields.

♥ Hearts ♦ Diamonds ♣ Clubs ♠ Spades
ID Card Val ID Card Val ID Card Val ID Card Val
0 2♥ 2 13 2♦ 2 26 2♣ 2 39 2♠ 2
1 3♥ 3 14 3♦ 3 27 3♣ 3 40 3♠ 3
2 4♥ 4 15 4♦ 4 28 4♣ 4 41 4♠ 4
3 5♥ 5 16 5♦ 5 29 5♣ 5 42 5♠ 5
4 6♥ 6 17 6♦ 6 30 6♣ 6 43 6♠ 6
5 7♥ 7 18 7♦ 7 31 7♣ 7 44 7♠ 7
6 8♥ 8 19 8♦ 8 32 8♣ 8 45 8♠ 8
7 9♥ 9 20 9♦ 9 33 9♣ 9 46 9♠ 9
8 10♥ 10 21 10♦ 10 34 10♣ 10 47 10♠ 10
9 J♥ 10 22 J♦ 10 35 J♣ 10 48 J♠ 10
10 Q♥ 10 23 Q♦ 10 36 Q♣ 10 49 Q♠ 10
11 K♥ 10 24 K♦ 10 37 K♣ 10 50 K♠ 10
12 A♥ 1/11 25 A♦ 1/11 38 A♣ 1/11 51 A♠ 1/11
// JavaScript card decoding
function decodeCard(id) {
    var suits = ['♥', '♦', '♣', '♠'];
    var ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
    return ranks[id % 13] + suits[Math.floor(id / 13)];
}
// decodeCard(10) → "Q♥" (Queen of Hearts)
// decodeCard(51) → "A♠" (Ace of Spades)

Result strings

Result Meaning
ongoing Round in progress
player wins Player wins
player wins with blackjack Natural 21 (1.5× payout)
dealer wins Dealer wins
dealer busted Dealer over 21
player busted Player over 21
push Tie

Integration Guide

Architecture

Portal
🌐 Frontend
Player Login
Game Lobby
Game iframe
3. Launch Game
▶
RGS
⚡ Game Server
load_game
play (bet/hit/stand)
Blackjack Logic
4. Validate + Bet/Win
▶
◀
balance
Platform
💰 Wallet
Player Auth
Balance
Transactions
1. Player logs in via Portal → Platform
2. Platform returns wallet_session_token
3. Portal launches game with token
4. RGS validates token & settles bets

Step 1: Player login (Platform API)

POST /api/portal/players/login
{
  "email": "player@example.com",
  "password": "password123",
  "portal_id": 1,
  "player_ip": "127.0.0.1"
}

// Response includes wallet_session_token

Step 2: Build game launch URL

https://rgs.cloudvlt.com/games/bj/play.html?token={wallet_session_token}&game_id=bj&rgs_url=https%3A%2F%2Frgs.cloudvlt.com%2Fapi%2Frgs&device_id=web

Step 3: Frontend implementation

// API helper
function api(params) {
    var body = Object.assign({}, params, { token: window.RGS_TOKEN });
    return fetch(window.RGS_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: Object.keys(body).map(function(k) {
            return encodeURIComponent(k) + '=' + encodeURIComponent(body[k]);
        }).join('&')
    }).then(function(r) { return r.json(); });
}

// Load game
api({ rquest: 'load_game', game_id: 'bj' }).then(function(res) {
    state.gameSession = res.game_session;
    state.balance = parseFloat(res.accountsum);
});

// Deal
api({
    rquest: 'play',
    game_session: state.gameSession,
    game_id: 'bj',
    bet_amount: 10,
    continue: 0,
    action: 'bet'
}).then(handleResponse);

// Player action
api({
    rquest: 'play',
    game_session: state.gameSession,
    game_id: 'bj',
    continue: 1,
    action: 'hit'  // or 'stand', 'double', 'split', 'insurance'
}).then(handleResponse);

Step 4: Handle response

function handleResponse(res) {
    // Update balance
    state.balance = parseFloat(res.accountsum);

    // Parse cards
    state.dealerCards = res.dealercards.split(',').map(Number);
    state.playerCards = res.playercards ? res.playercards.split(',').map(Number) : [];

    // Check for split
    if (res.player_hand1) {
        state.split = true;
        state.hand1 = res.player_hand1.split(',').map(Number);
        state.hand2 = res.player_hand2.split(',').map(Number);
    }

    // Available actions
    state.actions = (res.actions || '').split(',').filter(Boolean);

    // Round complete?
    if (parseInt(res.status_id) === 0) {
        showOutcome(res.result, parseFloat(res.winamount));
        state.gameSession = res.new_game_session;  // For next round
    } else {
        showActionButtons(state.actions);
    }

    renderTable();
}

Interactive API Testers

Test the Blackjack API directly from this page:

▶ Try it
▶ Try it

Testing with cURL

# Load game
curl -X POST https://rgs.cloudvlt.com/api/rgs \
  -d "rquest=load_game&token=1|abc123...&game_id=bj"

# Deal ($5 bet)
curl -X POST https://rgs.cloudvlt.com/api/rgs \
  -d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&bet_amount=5&continue=0&action=bet"

# Hit
curl -X POST https://rgs.cloudvlt.com/api/rgs \
  -d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&continue=1&action=hit"

# Stand
curl -X POST https://rgs.cloudvlt.com/api/rgs \
  -d "rquest=play&token=1|abc123...&game_session=SESSION_ID&game_id=bj&continue=1&action=stand"

Full tutorial with platform setup, player creation, and deployment: docs/BLACKJACK-FRONTEND-TUTORIAL.md

Multiplayer Blackjack

Real-time multiplayer blackjack with synchronized rounds. Up to 7 players can sit at a table and play simultaneously against the dealer. Uses WebSocket for instant updates.

Live Demo: Blackjack Pro — Login and select "MULTIPLAYER" from the game menu. Open in multiple browser tabs to simulate multiple players at the same table.

Game Flow

The following diagram shows the complete flow from joining a table to completing a round:

Portal/Player RGS API WebSocket Other Players GET /tables Return table list POST /tables/{id}/join {seat, player_id, name} Create GameSession Assign seat Broadcast: joined See player joined BETTING PHASE POST /bet {amount} Record bet Broadcast: bet placed See bet amount ALL BETS PLACED Deal initial cards phase → player_turn Broadcast: cards dealt current_seat = 1 PLAYER TURN PHASE POST /action {hit | stand | double} Execute action Update hand/move next Broadcast: action new cards/values See action result Wait for turn ↺ Repeat for each player SETTLING PHASE Dealer plays Calculate results phase → completed Broadcast: results win/lose/push COMPLETED POST /new-round Reset → betting Broadcast: new round Ready to bet again
Player Request
RGS Processing
WebSocket Broadcast
Other Players

Tables API

List available tables

GET /api/multiplayer/blackjack/tables

Response:
{
    "success": true,
    "tables": [
        {
            "table_id": "table-1",
            "name": "VIP Table 1",
            "type": "vip",
            "min_bet": 1.00,
            "max_bet": 500.00,
            "seat_count": 7,
            "occupied_seats": 2,
            "available_seats": 4,
            "status": "waiting",
            "current_phase": "betting"
        }
    ]
}

Create a new table

POST /api/multiplayer/blackjack/tables
Content-Type: application/json

{
    "name": "My Table",        // optional, auto-generated if not provided
    "type": "standard",        // optional: standard, vip, turbo (default: standard)
    "min_bet": 1.00,           // optional, in currency units (default: 1.00)
    "max_bet": 500.00,         // optional, in currency units (default: 500.00)
    "seat_count": 7            // optional: 1-7 (default: 7)
}

Response (201 Created):
{
    "success": true,
    "table": {
        "table_id": "table-a1b2c3d4",
        "name": "My Table",
        "type": "standard",
        "min_bet": 1.00,
        "max_bet": 500.00,
        "seat_count": 7,
        "status": "waiting",
        "current_phase": "betting"
    }
}

Creates a new blackjack table with the specified configuration. The table is immediately available for players to join. All parameters are optional - sensible defaults are used.

Get table state

GET /api/multiplayer/blackjack/tables/{table_id}?player_id={player_id}

Response:
{
    "success": true,
    "table": {
        "table_id": "table-1",
        "name": "VIP Table 1",
        "current_phase": "player_turn",
        "current_seat": 2,
        "dealer_cards": [8],
        "dealer_value": null,
        "seats": [
            {
                "seat_number": 1,
                "player_id": "player_alice",
                "player_name": "Alice",
                "bet": 10.00,
                "cards": [15, 22],
                "hand_value": 17,
                "status": "standing",
                "is_current": false
            },
            {
                "seat_number": 2,
                "player_id": "player_bob",
                "player_name": "Bob",
                "bet": 25.00,
                "cards": [0, 9],
                "hand_value": 12,
                "status": "playing",
                "is_current": true,
                "is_me": true,
                "available_actions": ["hit", "stand", "double"]
            }
        ]
    }
}

Join a table

POST /api/multiplayer/blackjack/tables/{table_id}/join
Content-Type: application/json

{
    "seat_number": 3,
    "player_id": "player_charlie",
    "player_name": "Charlie",
    "token": "wallet-session-token"
}

Leave a table

POST /api/multiplayer/blackjack/tables/{table_id}/leave
Content-Type: application/json

{
    "seat_number": 3,
    "player_id": "player_charlie"
}

Place a bet

POST /api/multiplayer/blackjack/tables/{table_id}/bet
Content-Type: application/json

{
    "seat_number": 3,
    "bet_amount": 25.00,
    "player_id": "player_charlie"
}

Player action

POST /api/multiplayer/blackjack/tables/{table_id}/action
Content-Type: application/json

{
    "seat_number": 3,
    "action": "hit",
    "player_id": "player_charlie"
}

// Valid actions: hit, stand, double, split, insurance

Testing with cURL

# List tables
curl https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables

# Create table
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Table","type":"standard","min_bet":5,"max_bet":500,"seat_count":6}'

# Join table
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/join \
  -H "Content-Type: application/json" \
  -d '{"seat_number":1,"player_id":"test123","player_name":"Tester"}'

# Place bet
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/bet \
  -H "Content-Type: application/json" \
  -d '{"seat_number":1,"bet_amount":25,"player_id":"test123"}'

# Perform action (hit/stand/double/split)
curl -X POST https://rgs.cloudvlt.com/api/multiplayer/blackjack/tables/{tableId}/action \
  -H "Content-Type: application/json" \
  -d '{"seat_number":1,"action":"hit","player_id":"test123"}'

Launch Game URL

https://rgs.cloudvlt.com/games/mbj/play.html?token={wallet_session_token}&game_id=mbj

Game Flow

Phase Description Actions
betting Players join table and place bets join, leave, bet
player_turn Each player acts in seat order (1→6) hit, stand, double, split, insurance
settling Dealer draws, hands are compared —
completed Results displayed, ready for new round new-round

Synchronized Round Flow

  1. Players join during betting phase and place bets
  2. Round starts: dealer deals 2 cards to each player with a bet, plus 2 to dealer (1 face down)
  3. Players act in seat order (1→6). Only the current seat can take actions
  4. After all players finish, dealer reveals hole card and draws to 17+
  5. Hands are settled, winnings credited
  6. Table resets to betting for next round

Real-time Updates (WebSocket)

The multiplayer blackjack uses Laravel Echo with Pusher/Reverb for real-time updates. All players at a table receive instant updates when any action occurs.

Channel

blackjack-table.{table_id}

Event: table.updated

// Broadcast payload
{
    "table": { /* full table state */ },
    "timestamp": "2026-09-18T14:30:00+00:00"
}

JavaScript Client Example

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key',
    cluster: 'mt1',
    forceTLS: true
});

// Subscribe to table updates
Echo.channel(`blackjack-table.${tableId}`)
    .listen('.table.updated', (e) => {
        console.log('Table updated:', e.table);
        updateUI(e.table);
    });

6-Deck Shoe

Multiplayer tables use a 6-deck shoe (312 cards) that is automatically reshuffled when fewer than 20 cards remain. Card IDs use the same 0-51 mapping as single-player blackjack, repeated 6 times in the shoe.