# Bounty Board Arcade SDK — agent-readable integration contract Human guide: https://www.bountyboard.gg/arcade/sdk Package: @bountyboard/arcade-sdk Release documented: 1.1.0 Wire protocol: 1 This file is the concise source for coding agents integrating an HTML5 game. The package TypeScript declarations are the exact public type reference. ## Non-negotiable integration rules 1. The SDK is never load-bearing. A game must boot and remain fully playable with no Bounty Board parent, no logged-in player, no ad inventory, no cloud save, and no multiplayer authority. 2. Call gameOver() exactly once per run and use integer scores. The host/server enforces per-game plausibility caps. 3. Bracket active play with gameplayStart()/gameplayStop(). Menus, pauses, death prompts, and ad breaks are not active play. 4. getPlayer() can be null and avatarUrl can be null. The SDK never exposes account ids, emails, or roles. 5. Store all progress in one save(string) blob (about 1 MB). Save at checkpoints/game over, not in a frame loop, and catch every rejection. 6. For new rewarded-ad work, prepare first, enable the game's button only when status is ready, call prepared.show() directly from the click/tap handler, and grant only when the final status is viewed. 7. Multiplayer clients send inputs, never state. One approved authority runs the simulation and sends viewer-safe snapshots. 8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before boot. ## Install and distribution Preferred for games with a build step: npm install @bountyboard/arcade-sdk import { BBArcade } from '@bountyboard/arcade-sdk'; The package is zero-dependency, typed, ESM + CommonJS, and SSR-safe. Importing the module does not install a window.BBArcade global. No-build script tag: This installs window.BBArcade. Standalone declarations: https://www.bountyboard.gg/arcade-sdk.d.ts Do not mix npm and the script tag in one page. A bundler that specifically wants the global may import @bountyboard/arcade-sdk/global. Multiplayer module import: import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer'; Script-tag multiplayer is BBArcade.multiplayer.joinRoom(...). ## Runtime capability matrix Distribution format does not determine capabilities; the embedding host does. - Bounty-hosted upload: lifecycle/scores supported; player identity/variants supported; cloud save/load available to logged-in players; ads available to approved games; multiplayer available to enabled game authorities. - Approved external URL embed inside the Bounty Board player: lifecycle/scores, identity, variants, approved Partner ads, and enabled multiplayer are supported; cloud save/load is unsupported. - Standalone or opened directly on the game's own site: fire-and-forget calls no-op; init resolves after a short grace; getPlayer returns null; getVariant returns the alphabetical control; host-only promise APIs reject unsupported or return an unavailable outcome. Guests are normal. getPlayer resolves null and account-backed calls may reject with code unauthenticated. ## Minimal correct lifecycle import { BBArcade } from '@bountyboard/arcade-sdk'; BBArcade.lockToHost({ allow: ['play.yourgame.com'] }); // optional, before boot void BBArcade.init(); // never gate boot on it BBArcade.gameLoadingFinished(); // first playable scene ready BBArcade.gameplayStart(); BBArcade.submitScore(Math.trunc(score)); // may repeat; host throttles BBArcade.gameplayStop(); // pause/death/menu/ad BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run For a shared-seed Daily, pass { mode: 'daily' } to submitScore and gameOver. ## API surface Lifecycle and scoring: - lockToHost({ allow?: string[], signed?: boolean, onBlocked?, redirect? }): void Early anti-rehosting check. Defaults allow Bounty Board hosts and localhost. - init({ rewardedAds? }?): Promise Announces the game and receives host config. Resolves when answered or after a short off-host grace; awaiting is optional. - configure(options?): void Applies the same module configuration without another host handshake. - ready() / gameLoadingFinished(): void Signals that the game is loaded and the player can start. - gameplayStart() / gameplayStop(): void Brackets active play. - submitScore(score: number, { mode?: 'daily' }?): void Current integer score; transport is throttled by the host. - gameOver(score: number, { mode?: 'daily' }?): void Final integer score; call exactly once per run. - xrSessionStart() / xrSessionEnd(): void Brackets immersive WebXR play. Player data and experiments: - save(blob: string): Promise One blob, about 1 MB. Requires a Bounty-hosted upload and logged-in player. - load(): Promise Returns null when no save exists. Rejects like save(). - getPlayer(): Promise<{ name: string, avatarUrl: string | null } | null> Display identity only; always handle null. - getVariant(key: string, variants: string[]): Promise Deterministic even split per player+game+key. Alphabetical first is the standalone/error control. Do not re-randomize client-side. Save/load rejection codes on Error.code: unsupported | unauthenticated | too_large | rejected | error Rewarded ads: - prepareRewardedAd(options?): Promise Recommended. Alias: prepareRewardedBreak(). ReadyHandle.show() is one-shot. - rewardedAd(options?): Promise Low-level structured-result API. Alias: showRewardedAd(). - rewardedBreak(options | onStart): Promise Deprecated compatibility helper. New games must use the prepared flow. - preloadRewardedAds(options?): Promise Warms the ads library. Alias: preloadRewardedAd(). Rewarded result status: viewed | dismissed | ready | unavailable | error Only final viewed grants. Useful error detail includes host_disabled, ad_break_unavailable, ad_break_timeout, no_rewarded_ad, ad_in_progress, show_ad_error, direct_user_action_required, before_reward_error, and ad_break_error. Multiplayer: - joinRoom({ code? | create?, joinData?, roomUrl?, ticket? }): Promise roomUrl+ticket are paired local-development overrides only. - Room fields: code, playerId, players, state, connected. - room.send(input): void Sends an input. Prefer trySend when disconnect races matter. - room.trySend(input): boolean Returns false when disconnected or when the socket raced closed. - room.on(event, handler): unsubscribe function Events: snapshot, playerJoin, playerLeave, event, end, error, connection, close. - room.leave(): void Intentional close with no reconnect. version: number is the wire-protocol version. ## Cloud-save recipe Hosted uploads have an opaque origin and no localStorage, so SDK save is the primary store there. URL embeds and standalone builds need their own same-origin fallback. const blob = JSON.stringify(progress); try { await BBArcade.save(blob); } catch (error) { if (error.code === 'unsupported') localStorage.setItem('progress', blob); } try { const blob = await BBArcade.load(); restore(blob ? JSON.parse(blob) : defaults); } catch { restoreStandaloneProgress(); } Do not assume load() resolves null for a guest; it can reject unauthenticated. ## Safe rewarded-ad recipe Prepare at the natural break. Keep the game-owned button disabled until ready. Call show() as the first operation in the direct click/tap handler: no await, timer, microtask, animation, state transition, or network call before it. const prepared = await BBArcade.prepareRewardedAd({ placement: 'death_revive', reward: 'extra_life', onStart: () => pauseGameAndAudio(), }); if (prepared.status === 'ready') { button.disabled = false; button.addEventListener('click', () => { const resultPromise = prepared.show(); // keep first button.disabled = true; void resultPromise.then(result => { resumeGameAndAudio(); if (result.status === 'viewed') revivePlayer(); else keepNormalFallbackAvailable(); }); }, { once: true }); } Never auto-trigger, loop, or auto-retry. Unavailable ad inventory on localhost is normal even though test mode is enabled automatically. ## Authoritative multiplayer contract (SDK 1.1) import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer'; const room = await joinRoom({ create: true, // or code: 'ABCD' joinData: { avatar: 'golem', mode: 'ffa' }, }); room.on('snapshot', ({ state }) => render(state)); room.on('connection', ({ connected }) => showReconnecting(!connected)); room.on('end', ({ results }) => showResults(results)); if (!room.trySend(input)) showReconnecting(true); joinData is an untrusted JSON object capped at 1 KiB. It must not contain secrets. The room authority validates admission and every input, runs the only simulation, clears held controls on disconnect, and sends viewer-safe state. Finite games may report server-authored results. Endless games do not invent a match_end. Multiplayer is curated per game slug. Bounty Board either hosts a game module or explicitly registers a mature external authority with a dedicated ticket key. External servers keep their existing simulation and add only an isolated signed-ticket adapter plus the SDK room envelope. Do not create a relay or a second simulation. External authority contract: https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt ## Verification checklist - Run the production artifact with no parent host; the whole game still works. - Verify gameLoadingFinished fires only when play is possible. - Verify gameplayStart/Stop on play, pause, resume, death prompt, and ad break. - Verify integer scoring, realistic caps, and one gameOver per run. - Verify guest, no-save, unsupported, too-large, and transport-failure save paths. - Verify getPlayer null and avatarUrl null. - Mock rewarded ready, unavailable, dismissed, error, and viewed; only viewed grants. - Assert prepared.show() is called directly from the player gesture. - Test multiplayer reconnect, trySend false, viewer-safe snapshots, and leave. - Test the identical artifact in its standalone location and inside Bounty Board. ## Troubleshooting map - Boot waits forever -> game startup depends on an SDK promise -> start init fire-and-forget and give every promise feature a standalone outcome. - save/load unsupported -> not a Bounty-hosted upload -> use own same-origin store. - save/load unauthenticated -> guest player -> continue with defaults/fallback. - direct_user_action_required -> show() lost browser activation -> make show() the first line of the click/tap handler. - host_disabled/unavailable -> host or inventory is not ready -> keep the normal fallback; inspect the live bb-arcade-host config before debugging Google. - joinRoom unsupported -> off-host or no approved authority -> hide multiplayer standalone and register/enable the authority. - trySend false -> disconnected/raced socket -> stop sending, show reconnecting, and resume from authoritative snapshots after connection.connected is true. ## More - Human guide: https://www.bountyboard.gg/arcade/sdk - Standalone types: https://www.bountyboard.gg/arcade-sdk.d.ts - Submit a game: https://www.bountyboard.gg/arcade/submit - Package README, changelog, AGENTS.md, design playbook, and external-authority guide ship inside @bountyboard/arcade-sdk.