MrXo SDK

Game Developer Documentation

Build, package, and ship browser games on MrXo β€” from the first script tag to admin review.

On this page

Introduction

A MrXo game is a plain HTML5 page. You write HTML, CSS and JavaScript exactly as you always do, add one script tag, and the platform handles player identity, scores, leaderboards, multiplayer rooms and cloud saves for you.

  • Player info β€” who is playing, for free
  • Scoring β€” submit scores and get an instant leaderboard rank
  • Multiplayer β€” real-time rooms with codes, host authority and matchmaking
  • Storage β€” persistent per-player, per-game key/value saves
  • Notifications β€” show toasts inside the game with one call

No backend, no accounts system, no WebSocket setup β€” everything goes through the SDK.

Quick start

The smallest possible game. Save as index.html, open your browser console and you should see the greeting:

index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /><title>My Game</title></head>
<body>
  <div id="game">Loading...</div>

  <script src="/sdk/game-sdk.js"></script>
  <script>
    Platform.onReady((ctx) => {
      document.getElementById("game").textContent =
        "Hello " + ctx.player.name + "! You are playing " + ctx.game.slug;
    });

    // Tell the platform your game finished loading
    Platform.ready();
  </script>
</body>
</html>

Two rules: include the SDK before your own code, and call Platform.ready() when your game has finished loading (the platform shows a spinner until you do).

  1. Sign in to MrXo and open Profile β†’ Become a developer
  2. Go to Studio β†’ New game and fill in the metadata
  3. Open Manage on your game and upload your .zip
  4. Click Submit for review β€” an admin approves it and it goes live

The platform context

When the platform boots your game it sends a context object to Platform.onReady:

FieldTypeDescription
ctx.game.slugstringYour game’s URL identifier
ctx.game.versionnumberCurrently running bundle version
ctx.game.isMultiplayerbooleanWhether multiplayer was enabled for this game
ctx.game.maxPlayersnumberMaximum players per room
ctx.player.idstringSigned-in player id
ctx.player.namestringPlayer display name
ctx.player.avatarUrlstring?Avatar URL, if the player has one
ctx.roomobject?Present only when launched into a room: { code, gameId, isHost }
ctx.tokenstringInternal handshake token β€” never use it directly
A guest (not signed in) gets name: "Guest" β€” your game should still work for them.

SDK API reference

Lifecycle

code
Platform.onReady((ctx) => {
  // Fires once, after the platform has initialized the SDK
});

Platform.ready(); // call when your game finished loading

Player

code
const player = Platform.getPlayer();
// { id: string, name: string, avatarUrl?: string }

Platform.onPlayerChange((player) => {
  // Called when the player info changes
});

Scoring

Platform.submitScore(value, metadata?) submits a score to the game’s leaderboard and resolves with the rank:

code
const result = await Platform.submitScore(1500, { difficulty: "hard" });
// { ok: true, rank: 42, value: 1500 }

if (result.rank === 1) Platform.notify("New record!", "success");
  • value must be a finite number
  • metadata is optional JSON for your own tracking
  • The player must be signed in (guests cannot submit)
  • Rejects after 10 seconds if the platform does not answer

Storage

Key/value storage scoped to this game and this player β€” use it for progress, settings and best scores:

code
await Platform.storage.set("progress", { level: 5, coins: 100 });
const progress = await Platform.storage.get("progress");
// { level: 5, coins: 100 }  β€” or null if unset

await Platform.storage.remove("progress");
  • Values must be JSON-serializable
  • get resolves null when the key does not exist (or after 8 seconds)
  • set/remove reject after 8 seconds if the platform does not answer

Multiplayer

Everything revolves around rooms. joinRoom resolves with the room you ended up in:

code
const room = await Platform.multiplayer.joinRoom({ mode: "auto" });
// { code: "AB12CD", gameId: "...", isHost: true }
OptionTypeDescription
mode"'auto' | 'create' | 'join'"auto joins an open lobby or creates one, create always makes a new room, join needs a code
codestring?Room code to join when mode: "join"

Once in a room you exchange messages and watch the roster:

code
// Send to everyone
Platform.multiplayer.send({ type: "jump", x: 10, y: 20 });

// Send to one player
Platform.multiplayer.send({ type: "hit", dmg: 3 }, targetPlayerId);

Platform.multiplayer.onPlayersChange((players) => {
  // Full player list after every join/leave
});

Platform.multiplayer.onMessage((msg, from) => {
  // msg = whatever the other player sent, from = { id, name, avatarUrl? }
});

Platform.multiplayer.onPlayerLeft((playerId) => {});
Platform.multiplayer.onRoomClosed((reason) => {}); // e.g. host ended the game

await Platform.multiplayer.leaveRoom();
const current = Platform.multiplayer.getRoom(); // Room | null
  • joinRoom rejects after 15 seconds (bad code, full room, no open lobby)
  • Payloads must be JSON-serializable β€” keep them small, this is realtime traffic
  • Room events also drive the platform lobby UI, so players see joins/leaves there too

Notifications

code
Platform.notify("Hello!", "info");     // "info" | "success" | "error"

Multiplayer guide

Enable multiplayer in your game’s metadata before uploading. Then decide how your game uses rooms:

  • Host authority β€” the player with room.isHost === true runs the real simulation and broadcasts state; everyone else sends inputs. This avoids desyncs.
  • Room codes β€” the code from joinRoom is shown in the platform lobby; players can share it to join privately.
  • Quick Play & matchmaking β€” players can be dropped into your game from the platform’s Quick Play; handle the case where ctx.room already exists on onReady.
  • Spectators / late joiners β€” send a full state snapshot to a new player on their first message.
code
Platform.onReady(async (ctx) => {
  // Might already be in a room when launched via Quick Play
  const room = ctx.room ?? await Platform.multiplayer.joinRoom({ mode: "auto" });

  if (room.isHost) {
    setInterval(() => broadcastState(), 100); // authoritative loop
  }
  Platform.ready();
});
Mark your game max players correctly when creating it β€” rooms are capped at ctx.game.maxPlayers.

Sandbox & security

Your game runs in an iframe with sandbox="allow-scripts" and an opaque origin. That is great for players (their session and data are isolated from your code) and has a few consequences:

  • ❌ No localStorage / sessionStorage / cookies β€” use Platform.storage
  • ❌ No access to the parent page’s DOM, cookies or JS
  • ❌ No reliable fetch() to external servers β€” bundle your assets into the zip
  • βœ… Your own scripts, assets and APIs inside the bundle work normally
  • βœ… window.Platform (postMessage bridge) always works
Calling window.parent.* or touching storage APIs throws a SecurityError β€” guard with try/catch or simply avoid them.

Packaging & upload

Zip the contents of your folder β€” index.html must sit at the root of the zip, not inside a nested folder:

my-game.zip
my-game.zip
β”œβ”€β”€ index.html        ← REQUIRED at the root
β”œβ”€β”€ game.js
β”œβ”€β”€ style.css
└── assets/
    β”œβ”€β”€ sprite.png
    └── sound.mp3

Limits: max 25 MB per zip Β· max 200 files Β· index.html at the root (index.htm also accepted).

Allowed file types:

KindExtensions
HTML.html .htm
JavaScript.js .mjs
CSS.css
Data.json .map
Images.png .jpg .jpeg .gif .webp .svg .ico
Fonts.woff .woff2 .ttf .otf .eot
Audio.wav .mp3 .ogg

Error codes you may get back:

CodeHTTPMeaning
NO_INDEX_HTML400No index.html at the zip root
ZIP_TOO_LARGE413Zip exceeds 25 MB
TOO_MANY_FILES400More than 200 files
DISALLOWED_EXT400A file has a disallowed extension
INVALID_ZIP400File is not a readable zip
EMPTY_ZIP400Zip has no files
INVALID_PATH400A path contains ..
FORBIDDEN403Not signed in as the game’s developer
Every upload creates version vN (previous versions stay available) and resets the game to Draft β€” so after a new upload, hit Submit for review again.

Publishing workflow

  1. Draft β€” create the game in Studio, upload the zip
  2. Submitted β€” click *Submit for review*
  3. Published β€” an admin approves it; it appears in Browse and Quick Play
  4. Rejected β€” the admin leaves a note; fix, re-upload, resubmit

Good metadata (clear title, description, tags, correct category and multiplayer flag) gets your game played β€” it is what Browse, search and Quick Play use.

Complete example

A finished single-player game: click counter with a saved best score, submitted to the leaderboard every 10 clicks.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Click Rush</title>
  <style>
    body { font-family: system-ui, sans-serif; display: grid; place-items: center;
           min-height: 100vh; margin: 0; background: #0f172a; color: #e2e8f0; }
    button { font-size: 2rem; padding: 1rem 2.5rem; border: 0; border-radius: 1rem;
             cursor: pointer; background: #f43f5e; color: white; }
  </style>
</head>
<body>
  <div style="text-align:center">
    <h1>Clicks: <span id="count">0</span></h1>
    <p>Best: <span id="best">0</span></p>
    <button id="btn">CLICK!</button>
  </div>

  <script src="/sdk/game-sdk.js"></script>
  <script>
    let clicks = 0, best = 0;
    const countEl = document.getElementById("count");
    const bestEl = document.getElementById("best");

    Platform.onReady(async (ctx) => {
      best = (await Platform.storage.get("best")) || 0;
      bestEl.textContent = best;
      Platform.ready(); // finished loading
    });

    document.getElementById("btn").addEventListener("click", async () => {
      clicks++;
      countEl.textContent = clicks;

      if (clicks > best) {
        best = clicks;
        bestEl.textContent = best;
        await Platform.storage.set("best", best); // persistent personal best
      }

      if (clicks % 10 === 0) {
        const res = await Platform.submitScore(clicks);
        Platform.notify("Score " + clicks + " submitted β€” rank #" + res.rank, "success");
      }
    });
  </script>
</body>
</html>

Multiplayer example

The skeleton of a real-time multiplayer game β€” join a room, keep the roster in sync, host-authoritative messages:

index.html (script part)
<script src="/sdk/game-sdk.js"></script>
<script>
  let room = null;

  Platform.onReady(async (ctx) => {
    try {
      room = ctx.room ?? await Platform.multiplayer.joinRoom({ mode: "auto" });
      document.getElementById("status").textContent =
        "Room " + room.code + (room.isHost ? " (host)" : "");
      Platform.ready();
    } catch (err) {
      Platform.notify(err.message, "error");
    }
  });

  Platform.multiplayer.onPlayersChange((players) => {
    document.getElementById("players").textContent = players.length + " players";
  });

  Platform.multiplayer.onMessage((msg, from) => {
    if (room.isHost) {
      // Host validates inputs and broadcasts authoritative state
      if (msg.type === "jump") applyJump(from.id, msg.x, msg.y);
    } else {
      // Non-host: render the state the host sent
      if (msg.type === "state") render(msg.snapshot);
    }
  });

  function jump(x, y) {
    Platform.multiplayer.send({ type: "jump", x: x, y: y });
  }
</script>

Sample games & testing

  • Published sample games on the platform (Click Rush, Reaction Duel, Memory Match) are complete, working references
  • Open your game in two browser windows to test multiplayer
  • Check the browser console for SDK warnings (e.g. messages dropped before onReady)
  • Verify scores on the game’s Leaderboard page and progress via Platform.storage
  • Test both a signed-in player and a guest

Browse published games β†’

Troubleshooting

  • `Platform` is undefined β€” the SDK script tag is missing or placed after your code. Load /sdk/game-sdk.js first.
  • `onReady` never fires β€” expected outside the platform (e.g. file://). Test through your Studio upload or Browse.
  • Messages are dropped with a console warning β€” you called an SDK method before onReady; move that code inside the callback.
  • `joinRoom` rejects after 15 s β€” wrong room code, room is full, or no open lobby to match into.
  • `submitScore` rejects β€” player not signed in, non-numeric value, or the platform did not answer within 10 s.
  • `storage.get` returns `null` β€” the key was never set (or the value failed to answer in 8 s).
  • Upload rejected β€” see the [error codes](#packaging): most often a nested folder hiding index.html or a disallowed file type.
  • No multiplayer β€” the game was created with the multiplayer flag off; metadata controls it.

Get the SDK

The SDK is a single self-contained file served by the platform β€” always load it from the URL below so you get protocol fixes automatically:

code
<script src="/sdk/game-sdk.js"></script>

Open /sdk/game-sdk.js

The SDK talks to the platform over postMessage β€” there is nothing else to install or configure.