@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) — theGameBridgeclass your game's client code uses (inside the operator's iframe).@moose/game-client-sdk/protocol— the message contract both sides agree on.GameBridgeuses this internally; you'd import it directly only if you're writing something that speaks the protocol withoutGameBridge(e.g. a test harness that plays the shell's part).@moose/game-client-sdk/behavior—BehaviorCollector, the pure interaction-timing aggregator behind bot-detection telemetry. Unrelated toGameBridge/protocol; it does nopostMessageing, and no network I/O either.@moose/game-client-sdk/behavior-reporter—BehaviorReporter, the network-attaching counterpart: owns aBehaviorCollectorand 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
new GameBridge(options: GameBridgeOptions)| Option | Type | Notes |
|---|---|---|
target | PostMessageTarget | Defaults 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.balanceMinoris 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 tonotifyBetEnd.notifyBetEnd(outcome: 'win' | 'loss' | 'declined' | 'rolled_back', amountMinor: number, balanceMinor: number, winAmountMinor?: number): void— call once your reveal has finished;balanceMinoris the final, fully-settled balance.winAmountMinoris only meaningful (and should only be passed) whenoutcome === 'win'.notifyBalanceExhausted(): voidnotifyExitGame(): 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 fromnotifyExitGame'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:
// 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
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 triggeringEventwhen available: a real click always hasisTrusted === true, while a programmatically dispatched one (element.click()from a script) hasisTrusted === falseand is flagged as an automation signal.ready(): boolean—trueonce 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.
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
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 underlyingBehaviorCollector. 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 (seeBehaviorCollector.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.visibilitychangeto'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:
event.originmatches the expected game origin exactly.event.sourceis the specific iframe you're bound to, not just any window at that origin.isBridgeEnvelope(event.data)— the payload carries theBRIDGE_SOURCEenvelope 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.