Arcade SDK docsCurated server integration

External authoritative multiplayer

Keep your existing multiplayer server as the single authority while the Arcade SDK handles Bounty Board admission, short lived room tickets, connection, and reconnect. This contract is game and hosting vendor neutral.

01

Overview

Use this path only when a mature server must remain responsible for admission, simulation, state, reconnects, and results.

Your server owns

Admission, game rules, input validation, simulation, viewer safe state, and real terminal results.

The SDK owns

Host ticket requests, the WebSocket lifecycle, bounded join data, reconnect, and a small transport envelope.

Contents

02

Registration

External authorities are reviewed and enabled per Arcade game. Registration is not self serve.

  • Exact Arcade game slug
  • Dedicated staging and production wss: endpoints
  • Room code format, maximum length, and whether a missing code may create a room
  • Allowed joinData fields and validation rules
  • Finite match or endless session result model
  • Operational contact for key rotation and incidents

Bounty Board provisions one dedicated server side ticket secret per game and environment. Never ship it in a browser bundle or reuse another game's room service credentials. Registered endpoints must use wss: outside loopback development and must not contain userinfo, query parameters, or a fragment.

Contents

03

Verify every ticket

The SDK receives a 60 second HMAC ticket from the trusted host. Your server verifies it before admitting the socket.

Ticket encoding
payloadB64 = base64url(UTF8(JSON(payload)))
signatureB64 = base64url(HMAC-SHA256(payloadB64, ticketSecret))
ticket = payloadB64 + "." + signatureB64
Ticket claims
{
  "v": 1,
  "sub": "player:<opaque-game-scoped-value>",
  "name": "Display Name",
  "avatarUrl": null,
  "slug": "your-game-slug",
  "roomId": "ROOM-CODE",
  "guest": false,
  "iat": 1700000000000,
  "exp": 1700000060000
}

Compare the HMAC in constant time, require protocol version 1, reject expired tickets, and match both slug and roomId exactly. iat and exp are Unix milliseconds. Treat sub as an opaque game scoped identity and never try to map it to a Bounty Board account. Guests get tickets too, with guest set to true and their own opaque subject. The display name and avatar are never authorization data, and avatarUrl may be null for any player.

Contents

04

Connect from the SDK

The public game API stays the same whether Bounty Board hosts the room module or connects to a registered external authority.

Join a room
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';

const room = await joinRoom({
  code: 'ROOM-CODE', // or create: true
  joinData: {
    schemaVersion: 1,
    loadout: 'starter',
    mode: 'friends',
  },
});

room.on('snapshot', ({ state }) => renderAuthoritativeState(state));
room.on('connection', ({ connected }) => setReconnecting(!connected));

if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
  setReconnecting(true);
}

Script tag builds call BBArcade.multiplayer.joinRoom(). Public quick match (match: true) is not available on an external authority: your server owns its capacity, lobby, and any matchmaking, so players join with code or create: true.

WebSocket URL
wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>

The SDK opens your registered endpoint with an encoded ticket and optional encoded join query; join is omitted when no joinData was supplied. joinData is untrusted JSON capped at 1 KiB. Keep the authenticated route separate from anonymous or legacy sockets, and redact request URIs from access logs because the ticket appears in the query string.

Contents

05

Room messages

All frames are JSON text. Send welcome only after ticket and admission validation, and keep every snapshot viewer safe.

Required welcome frame
{
  "t": "welcome",
  "playerId": "room-scoped-player-id",
  "players": [
    { "id": "room-scoped-player-id", "name": "Display Name", "avatarUrl": null }
  ],
  "state": { "phase": "lobby" }
}
Snapshots, events, and inputs
// Server -> client
{ "t": "snapshot", "tick": 42, "state": { "phase": "playing" } }
{ "t": "event", "data": { "type": "round_started", "round": 2 } }
{ "t": "player_leave", "playerId": "p2" }

// Client -> server: input only, never authoritative state
{ "t": "input", "seq": 7, "data": { "type": "move", "x": 1, "y": 0 } }

Send welcome within 10 seconds. The server may also send player_join, player_leave, event, error, and (only for finite games) match_end. Reject bad tickets with unauthenticated and invalid admission data with rejected, then close before welcome.

Contents

06

Reconnect safely

The SDK requests a fresh ticket for the same room and reuses the original joinData on each of its up to 3 reconnect attempts.

Rebind the seat from the verified (ticket.sub, ticket.roomId) pair, never from a client supplied player id or reconnect token. Clear held controls while disconnected and return the latest complete viewer safe state in the new welcome. Prediction based clients should receive the last processed input sequence so acknowledged inputs can be discarded.

Contents

07

Finite results only

A match ending message is a server authored fact, not a client request. Endless rooms must not invent one.

Finite match result
{
  "t": "match_end",
  "results": [
    {
      "playerId": "p1",
      "name": "Display Name",
      "placement": 1,
      "score": 1200,
      "outcome": "win"
    }
  ]
}

Persistent Bounty Board results, when enabled, use a separately provisioned server to server reporting credential. Never expose that credential to the game or accept client authored standings. Death, respawn, a round transition, or one player leaving does not automatically end a match.

Contents

08

Production checklist

Prove the trust boundary and failure paths before Bounty Board enables the authority in production.

  • Use separate endpoints and ticket secrets for staging and production.
  • Reject tampered, expired, wrong slug, and wrong room tickets before welcome.
  • Treat sub as opaque and name/avatarUrl as display only data.
  • Reject malformed, unknown, or oversized joinData; the limit is 1 KiB.
  • Validate every input and clear held controls on disconnect.
  • Send only viewer safe snapshots and bound every frame size.
  • Rebind reconnects from the fresh ticket, never a client identity claim.
  • Redact tickets and join payloads from logs and error telemetry.
  • Emit match_end only from a real server authoritative terminal condition.
  • Keep the game usable when multiplayer is unsupported or temporarily down.
  • Test both guest and logged in ticket subjects.
  • Test disconnect, held input cleanup, fresh ticket reconnect, and leave().
  • Rate limit upgrades, room creation, joins, and inputs.
  • Rotate the ticket secret without reusing another environment's key.
Contents