Skip to content

@moose/game-client-sdk reference

See the Reporting lifecycle events guide for a walkthrough.

The package has a root import and three subpath exports:

  • @moose/game-client-sdk (root) — the GameBridge class your game's client code uses (inside the operator's iframe).
  • @moose/game-client-sdk/protocol — the message contract both sides agree on. GameBridge uses this internally; you'd import it directly only if you're writing something that speaks the protocol without GameBridge (e.g. a test harness that plays the shell's part).
  • @moose/game-client-sdk/behaviorBehaviorCollector, the pure interaction-timing aggregator behind bot-detection telemetry. Unrelated to GameBridge/protocol; it does no postMessageing, and no network I/O either.
  • @moose/game-client-sdk/behavior-reporterBehaviorReporter, the network-attaching counterpart: owns a BehaviorCollector and posts its digests directly to the platform. This is what you actually use in a real integration — see Bot-detection signals.

All amounts (amountMinor, balanceMinor, winAmountMinor) are integers in the currency's minor unit (e.g. cents) — same convention as the wallet API.

Note that the player's requested language is not part of this protocol — GameBridge only carries the game → shell lifecycle events below. Language is an inbound, platform → game concern instead — see the launch URL's lang query parameter or VerifySessionResponse.config.language.

@moose/game-client-sdk

GameBridge

ts
new GameBridge(options: GameBridgeOptions)
OptionTypeNotes
targetPostMessageTargetDefaults to window.parent. Override mainly for tests.

Methods

  • notifyGameLoaded(balanceMinor: number): void — call this once, right after your game finishes loading, with the player's current balance (from your own session-verify/balance call) so the shell can render it immediately instead of waiting for the first bet to settle.
  • notifyBetStart(amountMinor: number, balanceMinor: number): void — call this once your round's result is already known (e.g. right after your server-side spin/settle call returns) and you're about to start revealing it — not before placing the bet. balanceMinor is the balance after this wager's own debit but before any win is applied, so a shell sees the debit immediately without your reveal animation being spoiled by an early win. Skip this call entirely for a declined bet — nothing was wagered — and go straight to notifyBetEnd.
  • notifyBetEnd(outcome: 'win' | 'loss' | 'declined' | 'rolled_back', amountMinor: number, balanceMinor: number, winAmountMinor?: number): void — call once your reveal has finished; balanceMinor is the final, fully-settled balance. winAmountMinor is only meaningful (and should only be passed) when outcome === 'win'.
  • notifyBalanceExhausted(): void
  • notifyExitGame(): void — the player clicked a "back to lobby" control inside the game; the game can't close the iframe it runs in, so this asks the shell to handle it.
  • notifySessionRevoked(reason?: string): void — an operator ended this player's session server-side (POST /v1/operator/players/kick) while the game was still open. The platform has no direct push channel into the browser — you learn about this through your own means (e.g. your next wallet call 401ing, or a server-push channel you operate yourself; see Session revoke webhook for the contract that notifies your server) — and report it here so the shell can pull the iframe down with a "you were logged out" message, distinct from notifyExitGame's voluntary "back to lobby".

@moose/game-client-sdk/protocol

The shell side has no wrapper class — an operator implements their own listener directly against this module (see the Reporting lifecycle events guide for the reference listener). Exports:

ts
// Game -> Shell — the only direction this protocol carries today
type GameEvent =
  | { type: 'GAME_LOADED'; balanceMinor: number }
  | { type: 'BET_START'; amountMinor: number; balanceMinor: number }
  | {
      type: 'BET_END'
      outcome: 'win' | 'loss' | 'declined' | 'rolled_back'
      amountMinor: number
      balanceMinor: number
      winAmountMinor?: number
    }
  | { type: 'BALANCE_EXHAUSTED' }
  | { type: 'EXIT_GAME' }

// Envelope every message is wrapped in
const BRIDGE_SOURCE = 'moose-platform-game-bridge'
type Envelope<T> = { source: typeof BRIDGE_SOURCE; payload: T }
function wrapEnvelope<T>(payload: T): Envelope<T>
function isBridgeEnvelope(data: unknown): data is Envelope<unknown>

// The minimal send surface GameBridge's target option accepts — real
// usage passes window.parent; tests inject a stub.
type PostMessageTarget = { postMessage(message: unknown, targetOrigin: string): void }

@moose/game-client-sdk/behavior

BehaviorCollector

ts
new BehaviorCollector(options?: BehaviorCollectorOptions)

type BehaviorCollectorOptions = {
  now?: () => number // injectable clock, for tests; defaults to Date.now
}

Aggregates raw browser interaction timing into a digest for the platform's bot-detection scoring. Does no network I/O by itself — pair it with BehaviorReporter below (or use BehaviorReporter directly, which owns its own collector) to actually post digests to the platform. See Reporting lifecycle events for a walkthrough.

Methods

  • recordInteraction(event?: { isTrusted: boolean }): void — call once per user-initiated game action (e.g. each spin button click). Pass the triggering Event when available: a real click always has isTrusted === true, while a programmatically dispatched one (element.click() from a script) has isTrusted === false and is flagged as an automation signal.
  • ready(): booleantrue once there's enough signal to be worth reporting: either an automation flag has already fired, or at least 3 interactions have been recorded (2 interactions give only 1 interval — no variance signal at all).
  • takeDigest(): BehaviorDigest — aggregates the current window into a digest and resets it, so the next digest reflects a fresh window instead of double-counting past interactions.
ts
type BehaviorDigest = {
  intervalMeanMs: number     // mean ms between recorded interactions
  intervalStdDevMs: number   // stddev — a bot's cadence tends to be fast and unnaturally regular
  automationFlags: string[]  // e.g. "navigator_webdriver", "untrusted_interaction_event"
}

Two automation flags are detected directly, without needing a timing window: navigator_webdriver (navigator.webdriver === true, set by most browser automation tools) and untrusted_interaction_event (any recorded interaction whose Event.isTrusted was false).

@moose/game-client-sdk/behavior-reporter

BehaviorReporter

ts
new BehaviorReporter(options: BehaviorReporterOptions)

type BehaviorReporterOptions = {
  ingestUrl: string           // the platform's behavior-ingestion endpoint — see Environments & base URLs
  sessionToken: string        // sent as the X-Session-Token header; this endpoint's only auth credential
  flushIntervalMs?: number    // defaults to 15000
  collector?: BehaviorCollector       // share one across multiple reporters, or inject a test instance
  collectorOptions?: BehaviorCollectorOptions // forwarded to `new BehaviorCollector()` when collector isn't given
  fetchImpl?: typeof fetch    // injectable, for tests. Defaults to the global fetch
}

Owns a BehaviorCollector and is the piece that actually talks to the network: it posts each digest straight to ingestUrl, with sessionToken as an X-Session-Token header — not a signed call like everything else in this SDK's ecosystem, since a browser can't hold a provider's HMAC secret. Everything is best-effort: a failed report (network error, missing fetch, an SSR/no-DOM environment) is swallowed silently and never surfaces to your game.

Methods

  • recordInteraction(event?: { isTrusted: boolean }): void — delegates to the underlying BehaviorCollector. Call once per user-initiated game action (e.g. each spin button click).
  • flush(): void — sends the current digest now if the collector is ready (see BehaviorCollector.ready), resetting its window either way. No-op if there isn't enough signal yet. Runs automatically on the flush timer and on tab-hide (document.visibilitychange to 'hidden') and page-unload (window.pagehide) — exposed directly only for a caller that wants to flush on its own trigger too.
  • stop(): void — cancels the flush timer and detaches the document/window listeners. Call this when the game is torn down (e.g. before navigating away from a SPA route), so a stale reporter doesn't keep firing.

Message safety

The shell side's own listener (implemented by the operator, not this package) is where inbound validation matters — every message it receives should be checked against three things before its payload is trusted:

  1. event.origin matches the expected game origin exactly.
  2. event.source is the specific iframe you're bound to, not just any window at that origin.
  3. isBridgeEnvelope(event.data) — the payload carries the BRIDGE_SOURCE envelope marker.

A message that fails any of these checks should be silently ignored, not thrown — this isn't a validate-or-throw model. Unrelated postMessage traffic on the same page (browser extensions, other embedded content) is expected and should be a no-op for your listener.