# External authoritative multiplayer servers Use this integration when your game already has a mature authoritative server that must remain responsible for admission, inputs, simulation, state, and results. Bounty Board connects the Arcade SDK to that server through a small adapter contract; it does not relay traffic or run a second simulation. External authorities are reviewed and enabled per game. Registration is not self-service. Contact Bounty Board before implementing the adapter so the game slug, endpoints, room-code rules, ticket keys, and result model can be agreed for staging and production. ## What stays authoritative Your server remains the only source of truth. It must: - admit a socket only after verifying its Bounty Board ticket; - treat `joinData` and every client input as untrusted; - run the only game simulation; - clear held controls and other transient input on disconnect; - send each player only the state that player may see; - decide when a finite match is over and author the final standings; and - keep endless rooms endless instead of inventing a terminal result. The SDK handles ticket acquisition, WebSocket connection, reconnection, and a small room-message envelope. It never grants a client authority over identity, state, roles, or results. ## Registration information Provide Bounty Board with the following for each environment: - the exact Arcade game slug; - one fixed `wss:` endpoint dedicated to authenticated SDK rooms; - the room-code format and maximum length your server accepts; - whether missing room codes may create rooms; - any validated, game-defined `joinData` fields; - whether the game has finite matches or endless sessions; and - an operational contact for key rotation or incident response. The registered endpoint must not contain userinfo, query parameters, or a fragment. Use `ws:` only for loopback development. Staging and production need different endpoints and different secrets. Bounty Board provisions a dedicated ticket-verification secret for the game and environment. Store it only on servers. Do not put it in the game bundle, browser storage, logs, analytics, crash reports, or client-visible environment variables. Do not reuse credentials from another game or room service. ## Connection flow 1. The game calls `joinRoom()` from the Arcade SDK. 2. The SDK asks the trusted Bounty Board host for a room ticket. 3. Bounty Board verifies the game approval, multiplayer approval, active play session, player or guest identity, game slug, and requested room code. 4. Bounty Board returns a 60-second ticket and the registered WebSocket URL. 5. The SDK appends the encoded ticket and optional encoded `joinData`, then opens the socket. 6. Your server verifies the ticket before admitting the socket and sends a `welcome` frame within 10 seconds. The sandboxed game never receives a Bounty Board session cookie and never calls the ticket HTTP endpoint itself. ## Ticket format Tickets are compact HMAC envelopes, not JWTs: ```text payloadB64 = base64url(UTF8(JSON(payload))) signatureB64 = base64url(HMAC-SHA256(payloadB64, ticketSecret)) ticket = payloadB64 + "." + signatureB64 ``` The HMAC input is the UTF-8 byte sequence of the base64url payload string. `iat` and `exp` are Unix milliseconds. The current lifetime is 60 seconds. ```json { "v": 1, "sub": "player:", "name": "Display Name", "avatarUrl": null, "slug": "your-game-slug", "roomId": "ROOM-CODE", "guest": false, "iat": 1700000000000, "exp": 1700000060000 } ``` Before accepting a connection, verify all of the following: - the ticket contains exactly one separator and both parts decode; - the HMAC matches using a constant-time comparison; - `v` is the supported ticket version; - `exp` is still in the future and `iat` is reasonable; - `slug` exactly matches the registered game; - `roomId` exactly matches the requested room and your registered rules; - `sub` is non-empty; and - required claim types and lengths are valid. Treat `sub` as an opaque game-scoped identity. Never attempt to map it to a Bounty Board account or correlate it with another game. `name` and `avatarUrl` are display data, not authorization data. `avatarUrl` may be `null` for any player. ## SDK call Module builds import multiplayer from its dedicated entry point: ```ts import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer'; const room = await joinRoom({ code: 'ROOM-CODE', // or create: true to ask the SDK for a new code 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 use `BBArcade.multiplayer.joinRoom(...)`. `joinData` is an optional JSON object capped at 1 KiB. Validate every field, reject unknown or oversized values, and never accept credentials or identity claims from it. If your server distinguishes create from join, use a documented game-defined intent field and rate-limit room creation. That field is still untrusted and grants no role or permission by itself. ## WebSocket URL The SDK opens the registered endpoint with URL-encoded query parameters: ```text wss://multiplayer.example.com/bountyboard?ticket=&join= ``` The `join` parameter is omitted when no `joinData` was supplied. Keep this path separate from any anonymous or legacy socket endpoint so a guessed room code cannot cross the authentication boundary. Redact or disable request-URI logging on this route because the short-lived ticket is in the query string. ## SDK room envelope All frames are JSON text. After successful verification and admission, send a `welcome` frame within 10 seconds: ```json { "t": "welcome", "playerId": "room-scoped-player-id", "players": [{ "id": "room-scoped-player-id", "name": "Display Name", "avatarUrl": null }], "state": { "phase": "lobby" } } ``` Send authoritative state snapshots as: ```json { "t": "snapshot", "tick": 42, "state": { "phase": "playing", "players": [] } } ``` Send game-defined transient events without changing their payload: ```json { "t": "event", "data": { "type": "round_started", "round": 2 } } ``` The SDK also recognizes player roster events: ```json { "t": "player_join", "player": { "id": "p2", "name": "Guest", "avatarUrl": null } } { "t": "player_leave", "playerId": "p2" } ``` Clients send inputs, never state: ```json { "t": "input", "seq": 7, "data": { "type": "move", "x": 1, "y": 0 } } ``` Validate the envelope, sequence, payload shape, value ranges, rate, current player state, and game rules before applying an input. Bound both inbound and outbound frame sizes. Reject an invalid ticket before `welcome` with an error and close the socket: ```json { "t": "error", "code": "unauthenticated" } ``` Use `rejected` for invalid admission or `joinData`. After admission, error codes remain game-defined strings and are surfaced through `room.on('error', ...)`. ## Reconnection The SDK automatically makes a small number of reconnect attempts. It requests a fresh ticket for the same room and reuses the original `joinData`. Rebind the seat using the verified `(ticket.sub, ticket.roomId)` pair. Never trust a client-supplied player id or reconnect token as the proof of identity. Send the latest complete viewer-safe state in the new `welcome` frame. If your client uses prediction, include the last processed input sequence in that state so it can discard acknowledged inputs. Clear held controls while the socket is absent. ## Finite matches and endless rooms Only a server-authoritative finite game may emit `match_end`: ```json { "t": "match_end", "results": [ { "playerId": "p1", "name": "Display Name", "placement": 1, "score": 1200, "outcome": "win" } ] } ``` The client receives this as `room.on('end', ({ results }) => ...)`. Persistent Bounty Board results, when enabled, use a separately provisioned server-to-server reporting credential. Never accept a client-originated result or reporting credential. Endless rooms must not emit `match_end`. Death, respawn, a round transition, or a player leaving is not automatically a terminal match result. ## Production checklist - Use an isolated staging endpoint and secrets before production. - Confirm tampered, expired, wrong-slug, and wrong-room tickets fail closed. - Test both guest and logged-in opaque subjects. - Reject malformed, unknown, or oversized `joinData`. - Send `welcome` only after ticket and admission validation succeeds. - Verify every snapshot is safe for its specific viewer. - Test disconnect, held-input cleanup, fresh-ticket reconnect, and `leave()`. - Rate-limit upgrades, room creation, joins, and inputs. - Redact tickets and join payloads from access logs and error telemetry. - Rotate the dedicated ticket secret without reusing another environment's key. - Emit final results only from a real authoritative terminal condition. - Keep the game playable when multiplayer is unsupported or temporarily down. For SDK integration basics, see https://www.bountyboard.gg/arcade/sdk. For the agent-readable API contract, see https://www.bountyboard.gg/arcade/sdk/llms.txt.