Submit a game

Bounty Board Arcade SDK

Add leaderboards, cloud saves, rewarded ads, player identity, experiments, and multiplayer lobbies to any HTML5 game, with optional authoritative rooms for trusted competition. The SDK has zero dependencies and every feature degrades safely when your game runs outside Bounty Board.

npm 1.4.12 stable · Protocol v1 · npm, ESM, CommonJS, and script tag

Build with your agent and invite friends to a playtest →

Script tagnpmllms.txt for agents

Integrating with Claude, Cursor, or another coding agent? Point it at /arcade/sdk/llms.txt, the full integration contract in one agent readable file.

01

Why integrate?

Your game already works without us. That is the point. Each SDK call you add turns anonymous plays into retention, revenue, and reach, and every feature is optional.

Leaderboards that bring players back

Three calls give your game real high score boards. Weekly top player boards run automatically, and guests who set a score get nudged into an account to claim it. Returning players, not one time visitors.

Rewarded ad revenue share

Eligible Arcade studios keep 90% of finalized ad revenue attributed to their game, settled monthly via Stripe once the $50 minimum is reached. Terms apply. Rewarded ads need a Bounty hosted build and admin approval. No AdSense account or ads ops on your side.

Multiplayer without the infrastructure

Approved games can start with signed rooms, public matchmaking, reconnect handling, and a built in relay lobby for 2 to 64 players with no server module. When wins or rewards depend on the rules, the rules have to run on a server: connect your own authoritative server, or ask on Discord about a referee module in Bounty Board rooms (today only first party games have one).

Cloud saves across devices

Uploaded builds run in a sandbox without localStorage. One save() blob (about 1 MB) follows logged in players across devices. Progress survives, players stay.

Experiments without a backend

getVariant() gives each player a stable A/B assignment per experiment key. Tune onboarding or difficulty with data instead of guesses.

Zero risk to your build

Zero dependencies, ESM + CommonJS + script tag, and every feature degrades safely off host. The same artifact ships to your own site and Bounty Board unchanged.

Contents

02

Quickstart

Use the npm package when your game has a build step. The package includes strict TypeScript declarations and is safe to import during SSR or prerendering.

Install
npm install @bountyboard/arcade-sdk
TypeScript quickstart
import { BBArcade } from '@bountyboard/arcade-sdk';

// Optional anti-rehosting check. Call before your game boots. Bounty Board and
// localhost are always allowed, so allow lists only the domains YOU host on.
// 'play.yourgame.com' is a placeholder. If we host your build, drop the allow
// list or the whole call.
BBArcade.lockToHost({ allow: ['play.yourgame.com'] });

// Starts the host handshake. Awaiting is optional and must not block boot.
void BBArcade.init();

// Wire these calls to your actual game state.
export function onGameReady() {
  BBArcade.gameLoadingFinished();
}

export function onRunStart() {
  BBArcade.gameplayStart();
}

export function onScoreChange(score: number) {
  BBArcade.submitScore(Math.trunc(score));
}

export function onPause() {
  BBArcade.gameplayStop();
}

export function onRunEnd(finalScore: number) {
  BBArcade.gameplayStop();
  BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
}

No build step? Use the script tag

It exposes the same API as window.BBArcade. Put it in your <head> ahead of your game bundle so the site lock check and host handshake start before your game boots. Do not use the npm package and script tag in the same page. Standalone declarations are available at /arcade-sdk.d.ts.

HTML script tag
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
<script>
  // The script installs window.BBArcade. Do not also bundle the npm package.

  // Optional site lock: 'play.yourgame.com' is a placeholder for YOUR domains.
  // Bounty Board and localhost are always allowed, so a build we host needs no
  // allow list: omit the option, or the whole call.
  BBArcade.lockToHost({ allow: ['play.yourgame.com'] });
  void BBArcade.init(); // never gate boot on it

  // Wire the rest to your actual game state. Loading alone is not an integration:
  // gameplayStart/gameplayStop bracket ACTIVE play (not menus) and drive your
  // playtime numbers, and gameOver closes the run.
  function onGameReady() {
    BBArcade.gameLoadingFinished(); // assets and first scene are playable
  }

  function onRunStart() {
    BBArcade.gameplayStart();
  }

  function onScoreChange(score) {
    BBArcade.submitScore(Math.trunc(score)); // repeat freely; the host throttles
  }

  function onRunEnd(finalScore) {
    BBArcade.gameplayStop();
    BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
  }
</script>

Let your agent integrate it

Connected to Bounty Board, your agent reads this contract by topic, checks the build against the upload rules before anything leaves your machine, and uploads it when it passes.

Claude Code
claude mcp add --transport http bounty-board https://www.bountyboard.gg/api/mcp
Codex
codex mcp add bounty-board --url https://www.bountyboard.gg/api/mcp
Add to Cursor

ChatGPT, Vercel Connect and any MCP client take the same URL: every client and your connections.

Contents

03

Choose an integration

Distribution format and runtime environment are separate decisions. npm versus script tag changes how code loads; the Bounty Board host determines which capabilities are available.

npm package (recommended)

Best for Vite, Phaser, React, Unity WebGL wrappers, and other bundled projects. Typed imports; multiplayer uses a dedicated subpath.

Script tag

Best for plain HTML/JavaScript builds. It installs window.BBArcade, including BBArcade.multiplayer, and the localStorage shim. A bundler that needs the global can import @bountyboard/arcade-sdk/global instead.

SDK feature support by runtime environment
CapabilityBounty hosted uploadApproved URL embedStandalone / own site
Lifecycle, scores, and host analyticsSupportedSupported in the Bounty Board playerDoes nothing
Player display identityLogged in player or nullLogged in player or nullnull
A/B variantsStable host assignmentStable host assignmentAlphabetical control
Cloud save / loadLogged in playersUnsupportedUnsupported
Native localStorageThrows (opaque origin), use the storage shimWorks on your own originWorks
Rewarded adsApproved gamesUnavailable (no payable attribution yet)Unavailable
Multiplayer roomsPer game approval (shared service relay available)Per game approval (shared service relay available)Unsupported

“Approved URL embed” means your externally hosted game is running inside the Bounty Board player. Opening the same URL directly is standalone play. Guests are also normal: identity resolves null and account backed features may reject as unauthenticated.

Contents

04

Core lifecycle

Wire these transitions to real game states. Missing stop events inflate active play signals; duplicate game over events create bad score data.

  1. 1Before bootlockToHost() if you use site lock, then init().
  2. 2PlayablegameLoadingFinished() after assets and the first scene are ready.
  3. 3Run startsgameplayStart(); submitScore() as the integer score changes.
  4. 4PausedgameplayStop() on pause, death prompts, menus, and ad breaks.
  5. 5Run endsgameplayStop(), then gameOver(finalScore) exactly once.
  6. 6Checkpointsave() progress outside the hot loop; always catch rejections.
Contents

05

Feature recipes

Add only the capabilities your game needs. Each recipe includes the failure path that should be part of the first implementation, not a later cleanup.

Scores and game over

Submit integer score snapshots freely; the host throttles transport. Call gameOver() exactly once with the final score. For a shared seed daily mode, pass { mode: 'daily' } to both calls. Fractional scores are truncated toward zero. Scores post only when the game declares Leaderboards, and the server rejects any score above the game's ceiling: a max score, or a per second rate for endless games, with a generous default when neither is set.

Cloud saves

Serialize the complete progress model into one string and save at checkpoints or game over, never every frame. Hosted uploads run in an opaque origin sandbox without localStorage, so SDK save is primary there. URL embeds and standalone builds need their own same origin fallback.

Gate the account calls on getPlayer(), which resolves null for guests and never rejects or hangs. That keeps the try/catch for real transport failures instead of turning a signed out player into an error you have to remember to swallow.

Cloud save with fallback
import type { BBArcadeError } from '@bountyboard/arcade-sdk';

function writeLocalProgress(blob: string) {
  try {
    localStorage.setItem('progress', blob);
  } catch {
    // Hosted uploads use an opaque origin, so localStorage may be unavailable.
  }
}

function readLocalProgress(): Progress | null {
  try {
    const blob = localStorage.getItem('progress');
    return blob ? JSON.parse(blob) : null;
  } catch {
    return null;
  }
}

// Ask who is playing first. getPlayer() resolves null for guests and never
// rejects, so the guest case is a branch instead of a thrown error.
export async function loadProgress(): Promise<Progress | null> {
  const player = await BBArcade.getPlayer();
  if (!player) return readLocalProgress(); // and offer "Sign in to save"

  try {
    const blob = await BBArcade.load(); // null means no save yet
    return blob ? JSON.parse(blob) : null;
  } catch {
    return readLocalProgress();
  }
}

export async function saveProgress(progress: Progress) {
  const blob = JSON.stringify(progress); // one blob, about 1 MB max

  try {
    await BBArcade.save(blob);
  } catch (error) {
    const code = (error as BBArcadeError).code;

    // Normal outside a Bounty hosted upload. Use your own same-origin store.
    if (code === 'unsupported') writeLocalProgress(blob);
    // A guest reached save() without the gate above. Never report success.
    else if (code === 'unauthenticated') offerSignIn();
    else reportSaveFailure(code);
  }
}

Rejection codes: unsupported, unauthenticated, too_large, rejected, and error. save() rejects for a guest on purpose: a write that did not happen must never resolve as if it had, and it is the one moment where you can offer to sign in and keep the progress.

Compare leaderboard and cloud save options for HTML5 games.

Engine exports: the localStorage shim

Prefer save()/load() when you control the source. The shim is for engine runtimes whose storage layer cannot be rewired without patching engine internals: GameMaker HTML5 (where ini_open, ini_write_*, and game_save all sit on localStorage), Godot, Unity, and Construct. It replaces the throwing localStorage with a Storage shaped object backed by your cloud save, so those builds persist per player and across devices with no engine changes.

Storage shim, script tag
<!-- Script tag: load order is the whole integration. The SDK installs the
     shim at load, so the engine below finds a working localStorage. -->
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
<script src="html5game/YourGame.js"></script>
Storage shim, module build
// Module builds: install it yourself, before any engine code runs.
import { BBArcade } from '@bountyboard/arcade-sdk';

BBArcade.storage.install();          // no-op where a real localStorage works

// getItem() is synchronous, but the cloud read that fills it is not.
const mode = await BBArcade.storage.ready();   // 'cloud' | 'memory' | 'native'
if (mode === 'memory') showGuestProgressNotice();

startGame();                         // now localStorage.getItem() sees the save

sessionStorage is shimmed too, but memory only. Guests and standalone play settle in memory mode: storage still works for the session, it is just never persisted. setItem throws QuotaExceededError past the ~1 MB save cap, like a real Storage.

Player identity and A/B variants

Player data is display only: name and optional avatar, never an account id, email, or role. getPlayer() reads the identity once at handshake time; subscribe with onPlayerChange() to react when the player logs in or out mid session. Variants are stable per player, game, and key; name the alphabetically first option as the safe control because that is what standalone and error paths receive.

Identity and experiment
const player = await BBArcade.getPlayer();
if (player) {
  greet(player.name);
  if (player.avatarUrl) drawAvatar(player.avatarUrl);
}

// Fires only when the identity changes (login/logout mid session).
const unsubscribe = BBArcade.onPlayerChange(next => updateGreeting(next));

// Alphabetical first is the standalone/error control: "control" here.
const cta = await BBArcade.getVariant('start-cta', ['control', 'short-label']);
renderStartButton(cta ?? 'control');

Prepared rewarded ads

Prepare at a natural break and keep your Watch Ad control disabled until status is ready. The retained show() call must stay in the browser's direct click/tap stack. Grant only on the final viewed result, pause gameplay and audio in onStart, and never loop or retry automatically.

Rewarded revive
const button = document.querySelector<HTMLButtonElement>('#watch-ad')!;
button.disabled = true;

// Prepare when the defeat/reward panel opens.
const prepared = await BBArcade.prepareRewardedAd({
  placement: 'death_revive',
  reward: 'extra_life',
  onStart: () => pauseGameAndAudio(),
});

if (prepared.status === 'ready') {
  button.disabled = false;
  button.addEventListener('click', () => {
    // Keep this first. No await, timeout, or network call before show().
    const resultPromise = prepared.show();
    button.disabled = true;

    void resultPromise.then(result => {
      resumeGameAndAudio();
      if (result.status === 'viewed') revivePlayer();
      else keepNormalFallbackAvailable();
    });
  }, { once: true });
} else {
  keepNormalFallbackAvailable();
}

Multiplayer rooms and lobbies

Multiplayer is enabled per game and has three rails. An approved game on Bounty's shared service gets the built in casual relay. The relay owns signed admission, roster, reconnect grace, capacity, host succession, and public message fan out, but it does not referee game state or results. Competitive or reward bearing play needs a server that runs the rules: a referee module inside Bounty Board's room service (today only first party games have one; ask on Discord), or a registered external authority that validates inputs, runs the simulation, and sends viewer safe state.

The first seat in a relay room fixes its capacity with joinData.roomSize, an integer from 2 to 64; missing or invalid means 8. The oldest retained seat is host, including during the 15 second reconnect grace. Relay state is { mode: 'relay', hostId, size, dropped }. Every game payload is public to every player, including the sender: there are no private relay messages or hidden state.

Relay payloads arrive in 20 Hz batches as { type: 'relay', messages: [{ from, data }] }; host changes arrive as { type: 'relay_host', hostId }. A payload may be at most 1 KiB of UTF-8 JSON, with 15 messages per player and 120 per room each second. Byte/rate excess is silently dropped and increments state.dropped. Relay metadata snapshots are 1 Hz. Relay rooms are endless, never emit end, and never report client trusted outcomes to Bounty Board.

Referee modules declare a 1 to 64 player range; relay rooms use 2 to 64. Players can use match: true to enter the game's open public room, or create: true/code for a shareable four character invite (a matched room's code also works as an invite). Registered external authorities set their own reviewed capacity and run their own matchmaking, so match: true is unavailable there.

joinRoom() is room transport, not one universal ready/start lobby state machine. Relay has the host contract above; referee modules and external authorities define their own ready, team, spectator, kick, start, and rematch schemas. The platform selects the tier for the game; joinData cannot choose it. When the promise resolves, welcome state is already in room.state and the initial roster is in room.players; render both before waiting for later events. Quick match is capacity based and may late join a running room, so spectator or late entry behavior is tier/game defined.

Simulation backed state sync is server round trip based. The SDK does not provide client side prediction, interpolation, or rollback. Plan around roughly 50 to 150 ms from input to viewer safe snapshot; smooth rendering locally, but correct to the next referee snapshot. Relay game messages instead arrive in the public 20 Hz event batches described above.

Multiplayer room
import type { BBArcadeError } from '@bountyboard/arcade-sdk';
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';

try {
  const room = await joinRoom({
    create: true, // or code: 'ABCD', or match: true for public quick match
    // A relay founder may set roomSize to an integer from 2-64 (default 8).
    joinData: { roomSize: 4, avatar: 'golem' }, // always untrusted
  });

  // welcome already arrived: render these fields before waiting for an event.
  renderLobby(room.code, room.players, room.state);
  room.on('playerJoin', () => renderPlayers(room.players));
  room.on('playerLeave', () => renderPlayers(room.players));
  room.on('snapshot', ({ state }) => renderRoomState(state));
  room.on('event', event => handleRoomEvent(event));
  room.on('connection', ({ connected, reconnecting }) =>
    setConnectionState({ connected, reconnecting })
  );
  let ended = false;
  room.on('close', ({ reconnecting }) => {
    if (!reconnecting && !ended) showConnectionClosed();
  });
  room.on('end', ({ results }) => {
    ended = true;
    showResults(results); // finite refereed rooms only
  });

  onPlayerInput(input => {
    if (!room.trySend(input)) {
      if (!room.connected) setReconnecting(true);
      else showSendFailure(); // serialization/WebSocket.send failed locally
    }
  });
  onExit(() => room.leave());
} catch (error) {
  const { code } = error as BBArcadeError;
  showSoloFallback(code); // every BBArcadeErrorCode is an expected state
}

See the relay room wire guide for a focused implementation contract. Already have a mature server? Keep it as the single authority and add an isolated signed ticket adapter; read the public external authority contract before implementing the endpoint.

Site lock and WebXR

Site lock is an optional early anti rehosting check, not DRM. Bounty Board and localhost are always allowed, so allow is where you list the domains you host the build on yourself. A build we host needs no entries, and the domains below are placeholders. signed: true asks the host for a signed origin attestation and checks it against a public key built into the SDK; a missing host or an invalid attestation blocks. WebXR games should bracket immersive sessions so playtime does not disappear when the flat page becomes hidden.

Site lock and WebXR
// Site lock is a deterrent, not DRM. Run it before boot.
BBArcade.lockToHost({
  allow: ['play.yourgame.com', 'preview.yourgame.com'],
  signed: true,
});

// WebXR only: keep immersive playtime visible to the host.
session.addEventListener('end', () => BBArcade.xrSessionEnd());
BBArcade.xrSessionStart();
Contents

06

Test before submitting

A correct integration is proven in both hosted and unhosted conditions. Treat unsupported, guest, unavailable, and reconnecting as expected states.

  • The build boots, starts, pauses, resumes, and finishes with no Bounty Board host present.
  • gameLoadingFinished() fires only when the first playable scene is ready.
  • gameplayStart() and gameplayStop() bracket active play, not menus or tab open time.
  • Scores are integers, plausibility caps match the game, and gameOver() fires once per run.
  • Guest and unsupported save/load paths keep progress usable without hanging the game.
  • getPlayer() null and avatarUrl null both render cleanly.
  • Rewarded UI covers ready, unavailable, dismissed, error, and viewed; only viewed grants.
  • The rewarded show() call is the first action in the player click/tap handler.
  • Multiplayer handles trySend() false, reconnect, relay events or viewer safe referee snapshots, and leave().
  • The same production artifact works in its standalone location and inside Bounty Board.
Contents

07

Troubleshooting

Start from the result or error code. Most integration failures are environment boundaries or lost browser user activation, not transport bugs.

SymptomLikely causeFix
'BBArcade is not defined' and the script tag was blocked by COEPThe page is cross origin isolated (Cross-Origin-Embedder-Policy: require-corp), which many engine test servers turn on for SharedArrayBuffer. Chrome reports ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep and drops the download.The hosted file opts in with Cross-Origin-Resource-Policy: cross-origin, so the tag above works as written. If a proxy strips that header, add crossorigin="anonymous" to the tag or ship a copy of the file inside your build and load it from the same origin.
The game waits forever during bootGame startup is gated on an SDK promise or host only feature.Start init() without awaiting it and give every promise feature a standalone outcome.
Scores never reach the leaderboardThe game has not declared Leaderboards in its Arcade submission, so the host rejects every posted score. Scores above the score ceiling set on the game are rejected too. submitScore() does not wait for an answer, so the game sees no error.Tick Leaderboards under Arcade SDK features on the game (we tick it when we find the calls in your build or page, and you can edit the game later), check its score ceiling, then confirm gameOver() fires once per run with an integer score.
save() or load() rejects with 'unsupported'The game is a URL embed, self hosted, or otherwise outside a Bounty hosted upload.Use your own same origin storage there. SDK cloud save is primary only in hosted uploads.
save() or load() rejects with 'unauthenticated'The player is browsing as a guest.Gate on getPlayer() so the guest case is a branch instead of a catch. Continue with defaults or a local fallback, and offer sign in where progress is at stake; do not force login from the game loop.
save() rejects with 'rejected'The player saved more than 60 times in a minute, or the host refused the request as invalid.Save at checkpoints and game over, never per frame. The storage shim already batches its writes.
localStorage throws SecurityError in a hosted buildUploaded builds run on an opaque origin, where localStorage, sessionStorage, IndexedDB, and document.cookie all throw rather than returning empty.Call save()/load() when you control the source, or BBArcade.storage.install() for an engine export whose storage layer you cannot rewire.
Shimmed storage reads empty on the first framegetItem() is synchronous but the cloud read that fills it is not.await BBArcade.storage.ready() before restoring progress. Writes made before then are kept and merged, so nothing is lost.
Ad result says 'direct_user_action_required'prepared.show() ran after an await, timer, microtask, animation, or network call.Call show() directly as the first line of the click/tap handler.
Ad result says 'host_disabled' or 'unavailable'The host has not enabled ads for the game, no placement is available, or local test inventory is empty. Ads run only on Bounty hosted builds that declare Rewarded ads and are approved for them.Keep the ad free fallback visible. Confirm the game declares Rewarded ads and the live host config before debugging Google.
joinRoom() rejects with 'unsupported'The game is standalone/SSR, or its multiplayer room service is not configured.Hide or disable multiplayer, preserve solo play, and confirm per game enablement and the room deployment.
Inputs disappear during a reconnectThe socket is temporarily closed and trySend() correctly returned false.Show reconnecting UI, stop sending, and resume from server snapshots after connection=true.
joinRoom() rejects with 'rejected'Invalid options/joinData/code, the game does not declare Multiplayer or is not approved for it, rate limiting, a seat that already has two open connections, or authority admission failure. External authorities also reject match: true.Do not guess from the generic code. Use a specific error.detail when present; keep solo play available and avoid blind retries.
joinRoom() rejects with 'unauthenticated'The room server refused the room ticket before welcome.Keep solo play available and offer a manual retry, which fetches a fresh ticket. Never build or cache tickets in the game.
joinRoom() rejects with 'error'No welcome arrived within the timeout (10 seconds by default), the socket closed before welcome, the host ticket request failed, or leave() ran during the join.Offer a manual retry and keep solo play. Raise timeoutMs only when your authority is slow to admit.
Contents

08

Defaults at a glance

What you get without configuring anything. Every value here can be relied on in code review and QA.

SettingDefault
lockToHost() allowlistbountyboard.gg and its subdomains, the Bounty Board staging host, localhost, and 127.0.0.1 are always allowed. Your allow entries extend that list; each matches the host and its subdomains, and plain hosts, host:port, or full URLs all work.
lockToHost() host checkWhen browser signals cannot name the embedding page (hosted uploads), the SDK asks the host and blocks if nothing answers within about 5 seconds.
init() and getPlayer() waitResolve when the host answers, and at once when the game runs top level or on another site the browser can identify, like an itch.io or Newgrounds page. Only an embed whose parent page the browser cannot identify waits about 1.5 seconds.
Host request timeoutsave(), load(), and multiplayer ticket requests reject with 'error' after 15 seconds with no answer; getVariant() resolves the control instead. Top level or on another site the browser can identify, they reject 'unsupported' at once.
getVariant() fallbackThe alphabetically first variant. It is the control standalone and on any error. A host assignment needs at least two variants: one variant resolves itself, an empty list resolves null.
save() blob limitOne string blob of about 1 MB (1 MiB) per player per game. Oversized blobs reject 'too_large' before any request is sent.
save() rateUp to 60 saves per player per minute; beyond that save() rejects 'rejected'.
submitScore() transportThrottled by the host, safe to call on every score change.
Rewarded ad waitA placement waits 6 seconds for an ad before resolving 'unavailable'. Set rewardedAds.loadTimeoutMs in init() or loadTimeoutMs per call, up to 15 seconds.
Rewarded ad test modeOn for localhost and file: pages; the Bounty Board host sets it per environment and its setting wins.
Room codesFour characters, generated for you on create: true (ambiguous characters excluded).
Quick matchBounty shared service relay and referee rooms: match: true uses the current public room while a seat is safely available, otherwise rolls a new one. Conservative reservations can roll early; lost seat races retry up to twice.
Room sizeA relay founder chooses 2 to 64 with joinData.roomSize (default 8); referee modules declare 1 to 64; registered external authorities set their own reviewed limit.
Relay trafficPublic to every seat: 1 KiB per payload, 15 messages/player/second, and 120 messages/room/second. Limit drops are silent and increment state.dropped.
joinData limitUntrusted JSON capped at 1 KiB; the room authority validates it.
Room input guardEvery Bounty hosted room accepts client frames up to 4096 characters and up to 90 inputs per player per second; the rest are dropped. Relay payloads also have the tighter limits above.
Reconnect policyJoin welcome times out after 10 seconds. The SDK makes up to 3 reconnect attempts; Bounty hosted seats have a 15 second grace. room.leave() never reconnects.
Connections per seatAt most 2 open sockets per player seat on Bounty hosted rooms. A third is rejected with 'rejected' and does not retry.
Rewarded ad revenue shareEligible Arcade studios keep 90% of finalized ad revenue attributed to their game, settled monthly via Stripe once the $50 minimum is reached. Terms apply.
Contents

09

API reference

Canonical calls and aliases from the package declarations. For exact option and result unions, use the bundled TypeScript definitions or the standalone declaration file.

Lifecycle and scoring

SignatureReturnsBehavior
init(options?)Promise<void>Host config handshake. Resolves when the host answers, at once top level or on another site, or after about 1.5 seconds in an embed whose parent page the browser cannot identify. Options: { rewardedAds: { enabled, testMode, preload, loadTimeoutMs } }. Safe to call without awaiting.
configure(options?)voidApply init options without repeating the handshake.
ready() / gameLoadingFinished()voidThe game is loaded and the player can start.
gameplayStart() / gameplayStop()voidBracket active play, including resume and pause transitions.
submitScore(score, { mode?: 'daily' })voidCurrent integer score. The host throttles transport.
gameOver(score, { mode?: 'daily' })voidFinal integer score, exactly once per run.
xrSessionStart() / xrSessionEnd()voidBracket immersive WebXR sessions.

Player data and experiments

SignatureReturnsBehavior
save(blob)Promise<void>One string blob, about 1 MB. Hosted uploads and logged in players only.
load()Promise<string | null>null means no save. Rejections use the same error codes as save(). Gate on getPlayer() so a guest is a branch, not a catch.
storage.install()'native' | 'cloud' | 'memory'Replace a throwing localStorage with a cloud backed shim, for engine exports. Idempotent; does nothing where a real Storage works. The script tag calls it for you.
storage.ready()Promise<'native' | 'cloud' | 'memory'>Resolves once the cloud read has landed. Await before restoring progress at boot.
storage.flush() / storage.modePromise<void> / modeForce the debounced write out now; mode reports what is backing storage.
getPlayer()Promise<Player | null>Display name and optional avatar only; never ids, emails, or roles. Never rejects or hangs, so it doubles as the signed in check for save()/load().
onPlayerChange(handler)() => voidRuns only when the display identity changes, e.g. the player logs in or out mid session. Subscribing does not replay the current value (call getPlayer() for that); returns an unsubscribe function.
getVariant(key, variants)Promise<string | null>Stable even split per player, game, and key; alphabetical first is the fallback/control. Needs at least two variants for a host assignment. Results show per variant in your game analytics.

Rewarded ads

SignatureReturnsBehavior
prepareRewardedAd(options?)Promise<Ready | Failure>Recommended two stage flow. Alias: prepareRewardedBreak().
rewardedAd(options?)Promise<AdResult>Low level structured result. Alias: showRewardedAd().
rewardedBreak(options | onStart)Promise<boolean>Deprecated compatibility helper. Use the prepared flow for new work.
preloadRewardedAds(options?)Promise<boolean>Warm the ads library. Alias: preloadRewardedAd().

Security and multiplayer

SignatureReturnsBehavior
lockToHost({ allow?, signed?, onBlocked?, redirect? })voidEarly anti rehosting check; Bounty Board hosts and localhost are always allowed. signed: true also verifies a host signed origin attestation. A deterrent, not DRM.
joinRoom({ code? | create? | match?, joinData?, timeoutMs?, roomUrl?, ticket? })Promise<Room>Import from '@bountyboard/arcade-sdk/multiplayer'. Empty options/create generate a four character code; shared service match: true uses the open public room; joinData is untrusted and capped at 1 KiB. timeoutMs overrides the 10 second welcome wait (clamped 1000 to 60000 ms, reconnects included). roomUrl + ticket are paired local dev overrides.
room.code / playerId / players / state / connected / latencyMslive fieldsInitial lobby data is available when joinRoom resolves; player and snapshot events keep it current. latencyMs is a join handshake estimate for render smoothing, refreshed on every reconnect and null before the first welcome.
room.send(input)voidSend a JSON value: an input to referee rooms, or a public game message to relay rooms. Prefer trySend() when local write status matters.
room.trySend(input)booleanfalse means disconnected, raced closed, or the input failed JSON serialization.
room.on(event, handler)() => voidsnapshot, playerJoin, playerLeave, event, end, error, connection, or close.
room.leave()voidIntentional close with no automatic reconnect.
versionnumberCurrent postMessage wire protocol version, not the npm version. Also exported as PROTOCOL_VERSION.
Contents

10

Next steps

Contents