npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@energy8platform/stake-bridge

v0.2.0

Published

Drop-in host-side wrapper that lets a game built against @energy8platform/game-sdk run on Stake Engine

Readme

@energy8platform/stake-bridge

Drop-in host-side wrapper that lets a game written against @energy8platform/game-sdk run on Stake Engine without modification.

Why

Stake's RGS returns the whole round (base + free spins + bonuses + multipliers) in a single /wallet/play response — pre-generated by the math SDK as a "book". Our game-sdk protocol streams events one PLAY_REQUEST at a time. StakeBridge bridges this gap so game code stays the same on both platforms.

Architecture

StakeBridge runs in-process with the game (no extra iframe layer): the game's own Vite/webpack build imports StakeBridge, the per-game adapter, and CasinoGameSDK together. They communicate through the in-memory channel the SDK already uses for devMode — there's no postMessage on Stake.

┌────────── Game bundle (single Vite build) ─────────────────────────────┐
│                                                                         │
│  main.ts:                                                               │
│   ├─ if URL has Stake params:                                           │
│   │   new StakeBridge({ devMode: true, adapter, modeMap, gameId, … })   │
│   │   const sdk = new CasinoGameSDK({ devMode: true })                  │
│   └─ else (Energy8 iframe):                                             │
│       const sdk = new CasinoGameSDK()                                   │
│                                                                         │
│  StakeBridge ←→ MemoryChannel ←→ CasinoGameSDK                          │
│       │                                                                 │
│       └─ fetch ──→ Stake RGS  (/wallet/authenticate /play /balance      │
│                                /end-round, /bet/event)                  │
└─────────────────────────────────────────────────────────────────────────┘

The game's runtime code is identical regardless of platform — it always talks to CasinoGameSDK. Only the host wiring differs.

What the bridge owns vs. what the adapter owns

| StakeBridge (this package) | BookAdapter (per game) | |---|---| | URL parsing (live: sessionID/rgs_url/lang/device; replay: replay/game/version/mode/event/amount/currency) | splitRound(book, ctx) → BookSegment[] | | Authenticate / Play / EndRound / Event / Balance calls | resumeFrom(book, lastEvent, ctx) (optional) | | Replay: GET /bet/replay/{game}/{version}/{mode}/{event} + book caching for "Play Again" | enrichConfig(config) (optional) | | Money conversion (decimal ↔ minor units × 1 000 000) | | | Segment cursor + creditPending lifecycle | | | Bet validation against authenticate config | | | Idle balance polling (skipped in replay) | | | Auto /wallet/end-round before final segment of non-zero-payout rounds | | | Mid-round recovery via /bet/event markers | | | Retry with exponential backoff on idempotent endpoints (authenticate, balance, end-round, event, replay) — never on /wallet/play | | | connection:lost / :restored events propagated through SDK | | | Currency metadata (config.currency), social-mode flag, autoplay recommendations, disclaimer lines, jurisdiction flags surfaced via INIT.config | |

The adapter is the only game-specific piece.

Installation

npm install @energy8platform/game-sdk @energy8platform/stake-bridge

Quick start (Vite-built game)

// src/main.ts — single entry point for both Energy8 and Stake builds
import { CasinoGameSDK } from '@energy8platform/game-sdk';
import { runGame } from './game';

const params = new URLSearchParams(location.search);
const isStake = params.has('sessionID') && params.has('rgs_url');

if (isStake) {
  const [{ StakeBridge }, { default: adapter }] = await Promise.all([
    import('@energy8platform/stake-bridge'),
    import('./stake-adapter'),
  ]);
  new StakeBridge({
    devMode: true,
    adapter,
    modeMap: { spin: 'BASE', buy_bonus: 'BONUS' },
    gameId: 'sweet-bonanza',
  });
}

const sdk = new CasinoGameSDK({ devMode: isStake });
await sdk.ready();
runGame(sdk);

Vite splits the Stake import into its own chunk automatically — Energy8 builds don't ship the bridge code.

Writing a BookAdapter

// src/stake-adapter.ts
import type { BookAdapter, BookSegment } from '@energy8platform/stake-bridge';

interface MyBook {
  events: Array<
    | { type: 'reveal'; matrix: number[][]; win: number }
    | { type: 'free_spin'; matrix: number[][]; win: number; multiplier?: number }
    | { type: 'pick'; choices: string[] }
  >;
}

const adapter: BookAdapter<MyBook> = {
  splitRound(book, _ctx): BookSegment[] {
    return book.events.map((evt, i, arr) => {
      const isLast = i === arr.length - 1;
      const next = !isLast ? arr[i + 1].type : null;

      return {
        action: evt.type === 'reveal' ? 'spin' : evt.type,
        data: evt,
        winThisSegment: 'win' in evt ? evt.win : 0,
        nextActions: isLast
          ? ['spin', 'buy_bonus']
          : [next === 'free_spin' ? 'free_spin' : next!],
        progressMarker: `evt-${i}`,
      };
    });
  },

  // Optional: where to resume after a reconnect.
  resumeFrom(book, lastEvent) {
    if (!lastEvent) return 0;
    const idx = book.events.findIndex((_, i) => `evt-${i}` === lastEvent);
    return idx >= 0 ? idx + 1 : 0;
  },
};

export default adapter;

The bridge calls splitRound once per round and synthesises roundId, currency, gameId, balanceAfter, creditPending, totalWin, and the session object from segment positions.

new StakeBridge(options)

| Option | Type | Default | Description | |---|---|---|---| | devMode | boolean | false | Run in-process via MemoryChannel (recommended for Stake). When true, iframe is not required. | | iframe | HTMLIFrameElement | — | The game iframe (only when devMode is false). | | adapter | BookAdapter \| factory | (required) | The per-game adapter. Pass it directly via your bundler's import. | | adapterUrl | string | — | Escape hatch — dynamic-import the adapter from a URL. Use only if you can't bundle it. | | modeMap | { [action]: stakeMode, default? } | {} | Map our action names → Stake mode. Falls back to uppercased action. | | gameId | string | '' | Game identifier surfaced on PlayResult.gameId. | | assetsUrl | string | iframe.src (iframe mode) / '' (devMode) | Base URL surfaced to the game in INIT.assetsUrl. | | url | string \| URL \| Location | window.location.href | Source for Stake URL params. | | protocol | 'http' \| 'https' | 'https' | RGS protocol. | | enforceBetLevels | boolean | true | Reject bets that aren't in betLevels. | | targetOrigin | string | '*' | Forwarded to the underlying postMessage Bridge. Ignored when devMode is true. | | balancePollMs | number | 60000 | Idle balance polling cadence. 0 disables. | | debug | boolean | false | Verbose logging. |

Exports

import {
  StakeBridge,
  RGSClient, RGSError, API_MULTIPLIER, parseStakeUrl,
  loadAdapter, defaultAdapterUrl, resolveAdapter,
} from '@energy8platform/stake-bridge';

import type {
  StakeBridgeOptions, BookAdapter, BookSegment, RoundContext,
  StakeRound, StakeUrlParams,
  AdapterModule, AdapterFactoryOptions, ModeMap,
} from '@energy8platform/stake-bridge';

Replay mode

Stake launches a historical round via a different URL pattern:

?replay=true&game=...&version=...&mode=...&event=...&rgs_url=...&currency=...&amount=...

StakeBridge auto-detects this and switches modes — same new StakeBridge(...) call covers both paths:

const params = new URLSearchParams(location.search);
const isStakeOrReplay =
  (params.has('sessionID') && params.has('rgs_url')) ||
  params.has('replay');

if (isStakeOrReplay) {
  const [{ StakeBridge }, { default: adapter }] = await Promise.all([
    import('@energy8platform/stake-bridge'),
    import('./stake-adapter'),
  ]);
  new StakeBridge({ devMode: true, adapter, modeMap, gameId, debug });
}

In replay mode the bridge:

  • skips /wallet/authenticate (synthetic config: balance = 0, currency and betLevels = [amount] from URL)
  • on the first play() calls GET /bet/replay/{game}/{version}/{mode}/{event} and caches the book — every subsequent "Play Again" replays the same book without another network round-trip
  • runs the cached book through your BookAdapter.splitRound exactly like a live round, so the game animates identically
  • never calls /wallet/end-round, /bet/event, or balance polling
  • sets INIT.config.replayMode = true so the game can hide the balance, bet selector, autoplay and buy-bonus controls and surface a "Play / Play Again" CTA only

The game's only replay-specific code is reading config.replayMode and adjusting its UI.

Connection state events

Idempotent RGS calls (authenticate, balance, end-round, event, replay) auto-retry on network errors and 5xx with exponential backoff (default: 3 attempts, 200/600/1800 ms ±25% jitter). When the first attempt fails the bridge emits 'lost'; when a retry succeeds it emits 'restored'. Games subscribe via the SDK:

sdk.on('connectionStateChanged', ({ status, code }) => {
  if (status === 'lost') showReconnectOverlay(code);
  if (status === 'restored') hideReconnectOverlay();
});

⚠️ /wallet/play is never retried. It is not idempotent — a timed-out request may have been processed server-side, so a blind retry could double-bill the player. On a play timeout the bridge surfaces an RGSError to the game; the recommended recovery is to wait for 'restored', then re-call Authenticate to discover whether the round actually started, and use getState() to resume.

Currency, social, autoplay, disclaimer

These all flow through INIT.config so the game reads them once at boot. None of them are required — fields are simply absent on platforms that don't supply them.

| Field | Source | Notes | |---|---|---| | config.currency | Authenticate.balance.currency looked up against the bridge's currency table | Includes code, symbol, decimals, symbolAfter. Use formatAmount(value, meta) for display. | | config.autoplay | Derived from jurisdiction.disabledAutoplay | undefined when autoplay is disabled or in replay mode; otherwise { maxCount, requiredStops }. Recommendations only — no hard enforcement. | | config.socialMode | URL ?social=truejurisdiction.socialCasino | Game must wrap user-visible text via applySocialReplacements(...) exported from this package. | | config.replayMode | URL ?replay=true | Tell the UI to render replay layout. | | config.disclaimerLines | buildDisclaimer(...) from this package | Canonical Stake template (6 lines, current calendar year on the copyright). | | config.demo | URL ?demo=true | Free-play / demo session. | | config.jurisdiction | Authenticate.config.jurisdiction | Raw flags (disabledTurbo, displayRTP, minimumRoundDuration, …). |

import { formatAmount } from '@energy8platform/stake-bridge';

const { config } = await sdk.ready();
display.textContent = formatAmount(123.45, config.currency!);
// → '$123.45' for USD, '€123,45' for EUR (locale-dependent)

Disclaimer

config.disclaimerLines is filled by default with Stake's official template — six sentences plus a copyright line — so games don't need to ship their own copy.

const { config } = await sdk.ready();
infoScreen.append(...config.disclaimerLines!.map((line) => p(line)));
// →  Malfunction voids all wins and plays.
//    A consistent internet connection is required. In the event of …
//    The expected return is calculated over many plays.
//    The game display is not representative of any physical device …
//    Winnings are settled according to the amount received from the …
//    TM and © 2026 Stake Engine.

To override (e.g. localised wording or extra clauses), pass your own to buildDisclaimer({ override }) before constructing the bridge, or replace the lines in enrichConfig. The seven approval points are documented in src/disclaimer.ts.

Social mode

@energy8platform/stake-bridge ships Stake's full social-mode dictionary — 36 canonical replacements (betplay, bonus buybonus / feature, …). When config.socialMode === true, run user-visible strings through applySocialReplacements:

import { applySocialReplacements } from '@energy8platform/stake-bridge';

const { config } = await sdk.ready();
const wrap = config.socialMode ? applySocialReplacements : (s: string) => s;

button.textContent = wrap('Place your bets');
// social=true → 'Come and play / join in the game'
// social=false → 'Place your bets'

The helper sorts rules by length descending automatically, so 'bonus buy' resolves before 'buy' and 'pays out' before 'pays'. Casing is preserved (BET → PLAY, Bet → Play).

Demo mode

config.demo === true when the URL carries ?demo=true. Real balance is not affected. Games typically render a "DEMO" banner and may use a pre-set demo balance.

Stake's RGS — at a glance

Five POST endpoints, all driven by the URL parameters Stake passes to the iframe (sessionID, rgs_url, lang, device):

| Endpoint | Purpose | |---|---| | /wallet/authenticate | Start session → balance, bet config, jurisdiction flags, optional in-flight round. | | /wallet/balance | Refresh balance (the bridge polls every 60 s while idle). | | /wallet/play | Start a new round → balance + round{betID, payoutMultiplier, costMultiplier, state, active, mode}. state is the math-SDK book. | | /wallet/end-round | Finalise a non-zero-payout round. Auto-skipped for zero-payout rounds (RGS auto-completes those). | | /bet/event | Track in-progress markers for state recovery. |

Money is in integer minor units × 1_000_000. The bridge handles all conversion.

License

MIT