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.0 stable · Protocol v1 · npm, ESM, CommonJS, and script tag

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

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 drive-by plays.

Rewarded ad revenue, 90% yours

Approved Bounty hosted games join rewarded ad revenue share at a 90% default studio split, attributed per game and settled monthly via Stripe. 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 2-64 player relay lobby with no server module. Add a bespoke referee module for trusted rules and results, or connect a mature external authority.

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.

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>

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.

SDK feature support by runtime environment
CapabilityBounty hosted uploadApproved URL embedStandalone / own site
Lifecycle, scores, and host analyticsSupportedSupported in the Bounty Board playerSafe no-op
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.

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.

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. The server still enforces the plausibility caps configured for your game.

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.

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 auto-retry.

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 without a bespoke module 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 reviewed Bounty referee module 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-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-64 player range; relay rooms use 2-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-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. 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();

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.

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 same-origin.
The game waits forever during bootGame startup is gated on an SDK promise or host only feature.Start init() fire-and-forget 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. submitScore() is fire-and-forget, so the game sees no error.Tick Leaderboards on the game (in the submit dialog or by editing it later), 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.
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, no placement is available, or local test inventory is empty.Keep the non-ad fallback visible. Confirm 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, disabled approval, rate limiting, 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.

Defaults at a glance

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

SettingDefault
lockToHost() allowlistBounty Board hosts and localhost are always allowed; your allow entries extend that list.
getVariant() fallbackThe alphabetically first variant. It is the control standalone and on any error.
save() blob limitOne string blob of about 1 MB per player per game.
submitScore() transportThrottled by the host, safe to call on every score change.
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-64 with joinData.roomSize (default 8); referee modules declare 1-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.
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.
Rewarded ad revenue share90% of net revenue to the studio by default; Bounty Board admins can adjust per game.

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 after a short grace off host. Safe to fire-and-forget.
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; no-op 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; alphabetical first is the fallback/control.

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. 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 wire protocol version.

Next steps