Contents
MiraDock Game SDK Specification
Version: 1.0-draft Status: contract for Phase 1 and Phase 2 of the game platform build Audience: AI coding agents and humans building games for MiraDock, and the MiraDock engineers implementing the host side
This document is the product contract. Every game published to MiraDock is built to it, and every host feature (play page, bridge, shop, saves, leaderboards) is implemented to it. Changes are additive; nothing here is removed or changed in meaning without a major version bump.
1. What a game is
A MiraDock game is one self-contained HTML file.
- Maximum 2 MiB after sanitization.
- Inline
<script>and<style>only. External scripts are stripped at publish time. - Images and fonts may load from
https://origins (CDNs, data: URIs). Nothing else leaves the sandbox. - No network calls of any kind from game code.
fetch,XMLHttpRequest, andWebSocketare blocked by CSP. Everything a game needs from the platform comes throughwindow.MiraDock. - No
localStorage,sessionStorage,indexedDB, or cookies. The iframe runs withoutallow-same-origin; these APIs throw. UseMiraDock.save. - The file must declare the SDK version it targets:
<meta name="miradock-sdk" content="1">in<head>. - The game must call
MiraDock.ready()within 10 seconds of load. A game that does not is shown an error state and counted as broken.
Game metadata (title, description, genre tags, cover image, orientation, player count) is not in the HTML. It is supplied through the MCP publish tools (section 10) so a creator can change it without republishing the game file.
2. The runtime
The platform injects window.MiraDock into every game at render time. Games never install, import, or version the SDK themselves. All methods return Promises. All calls are relayed over postMessage to the host page, which performs the real work and holds the real state. Game code never sees keys, tokens, prices, or server responses directly.
const ctx = await MiraDock.ready();
// ctx.player, ctx.game, ctx.entitlements, ctx.capabilitiesMiraDock.version is the integer SDK version (1).
MiraDock.capabilities reports which subsystems this host supports: { saves, leaderboards, shop, donate, multiplayer }, each true or false. A game must check before relying on any of them. In v1, multiplayer is false.
Events: MiraDock.on(event, handler) and MiraDock.off(event, handler).
| Event | Fires when |
|---|---|
entitlements | The player's entitlements changed (a purchase completed, a consumable was used). Payload: the full entitlement list. |
player | The player signed in or out mid-session. Payload: the new player object. |
pause / resume | The host asks the game to pause (a checkout sheet opened, the tab was hidden) or resume. |
3. Player identity
ctx.player // { id, displayName, avatarUrl, isGuest }idis a stable, per-game pseudonymous id. The same MiraDock account gets the sameidin the same game forever, and a differentidin every other game. Creators cannot correlate a player across games.isGuestistruefor visitors who are not signed in. Guests can play. Guest saves live only for the session. Purchases, donations, and leaderboard submission require a signed-in player; calling them as a guest resolves with{ status: "signin_required" }after the host offers a sign-in prompt.displayNameandavatarUrlare the player's public MiraDock profile values. Never store or display anything else about a player.
4. Saves
Per player, per game, JSON-serializable values.
await MiraDock.save.set("progress", { level: 4, coins: 120 });
const v = await MiraDock.save.get("progress"); // null if unset
await MiraDock.save.delete("progress");
const keys = await MiraDock.save.list();Limits: 32 keys per player per game, 64 KiB per value, 512 KiB total. Writes are last-write-wins. Exceeding a limit rejects with { code: "limit_exceeded" }.
Guests: values are held in host memory for the session and discarded on leave. A game may show "sign in to keep your progress"; the host also offers this itself.
5. Leaderboards
Boards are declared by the creator through MCP (section 10), not created from game code. Each board has an id, a sort (desc for high score, asc for fastest time), and a period (alltime, weekly, daily).
await MiraDock.leaderboard.submit("highscore", 4200);
const top = await MiraDock.leaderboard.top("highscore", { limit: 10 });
// [{ rank, playerId, displayName, score, at }]
const me = await MiraDock.leaderboard.me("highscore");
// { rank, score } or nullThe host keeps a player's best score per board per period. Submissions are rate limited to 10 per minute per player. Leaderboards in v1 are unverified: the host cannot prove a score was earned. Games with paid rewards must not gate rewards on leaderboard position.
6. Shop and entitlements
Everything a creator sells is a SKU. SKUs are registered through MCP (section 10) with a sku id, name, description, kind, and priceCents in USD. Prices live in the platform, never in the game file. The game only needs to know the SKU ids it cares about.
| kind | What it means | Entitlement behavior |
|---|---|---|
item | A thing the player owns (a skin, a weapon) | Granted once, permanent |
access | Unlocks part of the game (a level pack, the full game) | Granted once, permanent |
consumable | A thing that gets used up (lives, boosts) | Quantity increments per purchase, decrements on consume |
donation | A tip with a player-chosen amount | Grants the supporter entitlement (section 7) |
const items = await MiraDock.shop.list();
// [{ sku, name, description, kind, priceCents, owned, quantity }]
const ents = await MiraDock.shop.entitlements();
// [{ sku, kind, quantity, grantedAt }]
const result = await MiraDock.shop.purchase("level-pack-2");
// { status: "purchased" | "cancelled" | "signin_required" | "error", entitlement? }
await MiraDock.shop.consume("extra-life", 1);
// { ok: true, quantity: 2 } or { ok: false, code: "insufficient" }Rules the game must follow:
- The host is the source of truth. Gate content on
ctx.entitlementsand theentitlementsevent, never on local variables set after a purchase call. A purchase is not complete until the entitlement appears in that list. purchase()opens a host-rendered checkout sheet on top of the game. The game receivespause, thenresumewhen the sheet closes. The game never renders prices or payment UI itself;shop.list()is for showing what exists, with the price the host reports.- Payment, wallet balance, refunds, and payouts are entirely host concerns. The game has no API for them and never will.
- A game may be fully free, free with donations, paid SKUs only, or paid SKUs plus donations. The creator chooses in the creator dashboard, not in game code.
7. Donations
A creator may enable donations on any game, including one with paid SKUs.
- The play page shows a "Support this creator" button outside the iframe, so it cannot be spoofed by game code.
- The game may also open the same sheet:
await MiraDock.donate.open({ suggestedCents: 300 }). Resolves{ status: "donated" | "cancelled" | "signin_required" }. - A completed donation grants a
supporterentitlement (kinddonation, quantity = number of donations). Games may read it to say thanks or unlock a cosmetic. A game must not paywall core content behindsupporter; that is whataccessSKUs are for. - The platform takes a percentage of every donation and every SKU purchase. Rates are platform configuration and are shown to creators in the dashboard; they are not exposed to game code.
8. Multiplayer (reserved for v2)
The interface is defined now so games written today need no rewrite. In v1, MiraDock.capabilities.multiplayer is false and every method rejects with { code: "unsupported" }.
const room = await MiraDock.multiplayer.create({ maxPlayers: 4 }); // { roomId, joinCode }
const room = await MiraDock.multiplayer.join(joinCode);
room.send(payload); // to all players; JSON, 4 KiB max
room.on("message", ({ from, payload }) => {});
room.on("players", (players) => {}); // [{ id, displayName }]
room.leave();v2 will be host-relayed and turn/event based. Games that need authoritative real-time simulation will be a later capability with its own spec section.
9. Errors and limits
Every rejected Promise rejects with { code, message }. Codes: unsupported, signin_required, limit_exceeded, insufficient, rate_limited, invalid_argument, not_found, timeout, error.
Every call times out at 10 seconds with { code: "timeout" }. Per-player rate limits: saves 60/min, leaderboard submit 10/min, shop calls 30/min, donate 5/min. Games should debounce saves.
10. Publishing: the MCP tools
Creators connect their AI coding tool to the MiraDock MCP server with an existing write token or OAuth client. The game tools are:
| Tool | Purpose |
|---|---|
get_game_sdk_spec | Returns this document. Agents call it first. |
validate_game | Lints an HTML file against section 1 and section 11 without publishing. Returns { ok, errors[], warnings[] }. Agents iterate until ok. |
publish_game | Creates a game: HTML plus metadata (title, slug, description, genreTags[], coverImageUrl, orientation, players). Returns the play URL. |
update_game | New HTML version and/or metadata changes for an existing game. Previous versions are kept. |
set_game_skus | Replaces the game's SKU list. Prices in cents. Existing entitlements for a removed SKU are kept; the SKU just cannot be bought again. |
set_game_leaderboards | Replaces the game's leaderboard definitions. |
| set_game_monetization | { donations: true|false } and, once the creator has a connected payout account, { shop: true|false }. | | get_game_stats | Plays, unique players, revenue by SKU, leaderboard sizes. |
Games have a status: draft (creator only), published (listed and playable), unlisted (playable by link), suspended (platform action). publish_game creates in draft unless publish: true is passed.
Paid SKUs and donations become purchasable only after the creator completes payout onboarding in the dashboard. Until then the shop tools accept the definitions and the play page shows nothing for sale.
11. Validation rules
validate_game enforces, in this order. Errors block publish; warnings do not.
Errors:
- File exceeds 2 MiB after sanitization.
- Missing
<meta name="miradock-sdk" content="1">. - External
<script src>present (it would be stripped, so the game would break). - No call to
MiraDock.ready(found in game script. - Any use of
localStorage,sessionStorage,indexedDB,document.cookie,fetch(,XMLHttpRequest, orWebSocketfound in game script. - A
MiraDock.method referenced that does not exist in this spec.
Warnings:
- No
<title>. - No
viewportmeta tag. - Uses
MiraDock.multiplayer(unsupported in v1). MiraDock.shop.orMiraDock.donate.referenced but the game has no SKUs / donations not enabled.- Save writes appear inside a
requestAnimationFrameor interval callback (likely to hit rate limits).
12. Minimal compliant game
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="miradock-sdk" content="1">
<title>Tap Counter</title>
<style>body{margin:0;font-family:system-ui;display:grid;place-items:center;height:100vh}</style>
</head>
<body>
<button id="tap">Taps: 0</button>
<script>
(async () => {
const ctx = await MiraDock.ready();
let taps = (await MiraDock.save.get("taps")) ?? 0;
const btn = document.getElementById("tap");
const render = () => btn.textContent = `Taps: ${taps} (${ctx.player.displayName})`;
render();
btn.onclick = async () => {
taps++;
render();
await MiraDock.save.set("taps", taps);
if (taps % 10 === 0 && MiraDock.capabilities.leaderboards) {
await MiraDock.leaderboard.submit("taps", taps);
}
};
})();
</script>
</body>
</html>13. Host implementation notes (not part of the creator contract)
- The bridge is a single
postMessagechannel with a per-load nonce, following the existing height-reporter pattern incomponents/wikis/HtmlPageRenderer.tsx. Every message carries{ nonce, id, method, args }and gets exactly one{ nonce, id, result | error }reply. - The injected SDK is a small script prepended to the sanitized
srcdoc, versioned with the platform, never fetched by the game. - Entitlement checks, save writes, and leaderboard writes are server actions authenticated by the host page's session, never by anything the game supplies.
player.idishmac(server_secret, user_id + game_id), truncated and base32 encoded.- Rate limits are enforced host-side per method; the game gets
rate_limited, never a silent drop.