Skip to main content

Arena integrations

Arena provides private, turn-based competitions for partner platforms. Your backend supplies questions and authenticates learners. The hosted service owns timing, scores, persistence and authorization.

The open-source package contains a TypeScript client, accessible player, WebSocket helper, OpenAPI description, tests and localhost demonstration server. See licensing scope.

Availability​

The hosted contract requires deployment of the Arena API and migration 20260925_0001_public_arena.sql. Publishing the SDK alone does not deploy the service. A 404 on /v1/arena/competitions may indicate that the target deployment does not yet contain this route. Use the local demonstration server to evaluate the player independently. GitHub publication does not imply npm publication.

This release supports turns-v1: one to six players, supplied text questions, participant/spectator sessions and durable completion events. It is separate from MirrorMingo's nationwide class competition. Public matchmaking, the branded 3D scene, chat, voice, generated questions, translation, curriculum access and general tutoring are outside this contract.

Ownership and credentials​

Organiser operations require Authorization: Bearer mirrormingo_…. Keep this key on your server. Keys owned by the same developer account share competitions; different accounts cannot read or change each other's competitions. Use separate developer accounts for independent tenant ownership. This version has no hosted test/live environment separation.

Arena access is represented by the developer product arena_api in the shared MirrorMingo entitlement catalogue. Deployments can enforce it with MIRRORMINGO_ARENA_REQUIRE_ENTITLEMENT=true; when enabled, a valid key without an active Arena Developer API entitlement receives 403. Keep this disabled only for an approved internal migration or pilot. Configure Paystack plan codes with PAYSTACK_PLAN_ARENA_API_MONTHLY and PAYSTACK_PLAN_ARENA_API_YEARLY, then use the normal developer subscription flow. This is separate from learner and school subscriptions.

Your backend must authenticate and authorize a learner before issuing a session. The browser receives only an expiring arena_… token scoped to one competition and role. Keep it in memory, never URLs, logs or persistent browser storage. Spectator sessions cannot answer and never create participant seats.

External learner IDs are hashed with the developer account and competition. Display names are visible to other participants and authorized spectators; use nicknames where appropriate. A new session for the same learner restores the same seat and issues a new token. It does not revoke previous tokens. Default expiry is one hour; allowed TTL is 60–86400 seconds. Organisers can revoke all sessions immediately. Expiry and revocation also terminate active sockets.

Server integration​

import { MirrorMingo } from "@mirrormingo/sdk";
const mm = new MirrorMingo({ apiKey: process.env.MIRRORMINGO_API_KEY });
const match = await mm.arena.create({
request_id: "class-maths-2026-09-25",
title: "Maths challenge", max_players: 2, seconds_per_question: 30,
questions: [
{ id: "q1", prompt: "2 + 2 = ?", options: ["4", "5"], correct_index: 0 },
{ id: "q2", prompt: "3 + 3 = ?", options: ["5", "6"], correct_index: 1 },
],
});
const ada = await mm.arena.createSession(match.id, {
learner_id: "student-101", display_name: "Ada",
});
const musa = await mm.arena.createSession(match.id, {
learner_id: "student-102", display_name: "Musa",
});
const audience = await mm.arena.createSession(match.id, { role: "spectator" });
// Deliver each token only to its intended authenticated recipient.
await mm.arena.start(match.id);

Provide questions you have permission to use: unique IDs, two to six distinct nonempty options and a zero-based correct index. Up to 120 questions are accepted. Partner questions are not automatically verified or endorsed. Content is immutable after creation; create another competition to change it.

Embed a player​

Build the standalone @mirrormingo/arena package from the public repository, or import mountArena from the updated main SDK. Registry publication is separate.

import { mountArena } from "@mirrormingo/arena";
const session = await fetch("/api/my-arena-session", { method: "POST" }).then(r => r.json());
const player = mountArena(document.getElementById("arena")!, {
session,
onComplete: state => console.log(state.phase, state.players),
onError: error => console.error(error),
});
// On component/page teardown: player.destroy();

The player uses semantic HTML, keyboard-operable buttons, text-only content rendering and no React/Three.js dependency. Style .mm-arena in your application. For React, mount inside an effect and return player.destroy as its cleanup. Polling runs every second, backs off after failures and stops on terminal state or invalid credentials. Obtain a fresh session from your backend after expiry. Reconnection does not pause or extend a turn. Live competitive play is online-only.

Rules and lifecycle​

waiting → running → reveal → running → … → finished, or cancelled.

  • The organiser starts explicitly. Optional opens_at and closes_at are timezone-aware ISO 8601 timestamps enforced on the server.
  • Registration closes at start. Questions must divide equally among the actual registered players, ensuring equal turn counts. Seat order is stable and turns alternate round-robin. Partners must balance difficulty across seats themselves.
  • Turn windows are 10–300 seconds. Correct answers score 100; wrong answers and timeouts score zero. Each result has a three-second reveal.
  • Ranking uses score, then total elapsed answer time, then stable participant ID. Timeouts consume the full window. Players who never answer have no final rank.
  • Snapshots withhold future prompts and all unrevealed answers. Only the current question is shown; its answer/explanation appears after submission or timeout.
  • Deadlines survive disconnects/restarts. PostgreSQL serializes transitions and a lifespan clock worker advances inactive matches. Redis/sticky sessions are unnecessary for this public format.
  • Starting requires enough remaining time for all full windows and reveals. Cancellation does not produce a completed ranked result.

HTTP reference​

Paths are relative to https://api.mirrormingo.com/v1.

MethodPathCredentialSDK
POST/arena/competitionsDeveloper keyarena.create(input)
GET/arena/competitions/{id}Developer keyarena.get(id)
POST/arena/competitions/{id}/sessionsDeveloper keyarena.createSession(id, input)
POST/arena/competitions/{id}/startDeveloper keyarena.start(id)
POST/arena/competitions/{id}/cancelDeveloper keyarena.cancel(id)
POST/arena/competitions/{id}/revoke-sessionsDeveloper keyarena.revokeSessions(id)
GET/arena/play/{id}Arena sessionarena.state(id)
POST/arena/play/{id}/answersParticipant sessionarena.answer(id, input)
GET/arena/events?after=0&limit=50Developer keyarena.events(after, limit)

The API reference defines all schemas. Timestamps/deadlines in snapshots are Unix seconds. Use server_time to compensate for device clock skew. version increases only when persisted state changes.

WebSocket protocol​

Connect to /v1/arena/play/{id}/ws over WSS and send within ten seconds:

{"type":"hello","token":"arena_SESSION_TOKEN"}

Receive {"type":"state","state":{...}} on version changes, otherwise {"type":"heartbeat","server_time":1234567890}. Reconnection returns the current snapshot, not missed intermediate messages. Submit answers over HTTP for explicit acknowledgements. Invalid/expired/revoked credentials close with 4401; terminal states close normally with 1000.

import { connectArena } from "@mirrormingo/arena";
const disconnect = connectArena({
getSession: () => fetch("/api/my-arena-session", { method: "POST" }).then(r => r.json()),
onState: state => renderYourUI(state),
onError: error => showError(error),
});

The helper reconnects with backoff after transport failures, requests credentials again, detects missing heartbeats, and stops on authorization failure. It never sends a token in a URL. Call the returned function to disconnect.

Retries and trusted results​

Creation is idempotent by (developer account, request_id); answers by (competition, participant, request_id). Reusing an ID with changed input returns 409. An answer retry returns current state without awarding duplicate points. Persist IDs across retries. The SDK never automatically retries mutations. Session creation deliberately returns a fresh token.

Errors: 401 invalid credential; 403 spectator cannot answer; 404 missing/unowned competition; 409 stale question, wrong turn, full roster or state conflict; 422 invalid input; 429 rate limit. Refresh state before retrying a 409. Respect Retry-After. Existing HTTP rate limiting applies when enabled by the deployment; it is not a billing quota. Configure production request/connection limits at the gateway. A live socket sends at most one snapshot/heartbeat per second.

Finished/cancelled matches produce durable arena.finished/arena.cancelled events. Poll /arena/events, process event_id idempotently, then persist the returned cursor. Continue while has_more is true. Events contain the final snapshot and are isolated by developer account. Reconcile with arena.get(id). The existing exam webhook dispatcher does not deliver Arena events in this version. Browser completion callbacks are for UI; consume server events for grades.

Deployment and tests​

Apply the migration before deploying. The application mounts /v1/arena and starts the clock maintenance task in its lifespan. Tests cover PostgreSQL ownership, concurrency, replay, expiry and completion, plus SDK transport and player behaviour. Validate your deployed gateway, authentication, WebSocket proxy and actual devices separately. No production acceptance is implied by local tests.