# Mmorada for developers

> Bring an HTML5 game to Mmorada, the Spanish-language home for people who play: a library of browser games with cloud saves, achievements, leaderboards, presence, a store, multiplayer rooms, and a community (a forum, Reddit-style) around every game. This file is written for coding agents: read it whole, then follow "Porting checklist".

Site: https://www.mmorada.com/ · Library: https://www.mmorada.com/apps · Developer portal: https://www.mmorada.com/dev (Spanish UI, sign in with a Mmorada account).

## What Mmorada is, in one minute

- Mmorada hosts static browser games. A game is a folder with `index.html` at its root; the platform serves it from its own origin (`https://play.mmorada.app`) inside the site's play page, sandboxed. No servers of yours are needed; a game may still talk to its own backend (declare its origins on the portal).
- Players sign in once, with their Mmorada account. The game learns who plays through the SDK (`Mmorada.user`), never through passwords or OAuth of its own.
- Everything a game gives its players lives on the platform: cloud saves, achievements, leaderboards, "playing now" presence, a store (the game itself and items inside it), feedback, error reports, multiplayer rooms.
- Spanish first: players read Mmorada in Spanish (the interface is machine-translated to ~30 languages for others). `ctx.lang` tells the game the player's language; ship Spanish text, at least.
- Money: the developer keeps 80 % of each sale after the payment processor's 5 %. Paid once a month, 30 days after the sale, once US$100 is owed. A game costs between US$1 and US$20, or is free; items between US$0.25 and US$20. No loot boxes, no ads inside games, nothing sold that gives an advantage over other players. Players may refund a game within 14 days if they played under 120 minutes.

## What a build must be

- A static folder: `index.html` at the root, every path relative and case-exact (the storage is case-sensitive), no server-side code.
- At most 500 MB and 1500 files per build; the first screen (what `index.html` loads before anything is playable) under 50 MB. Precompressed engine files (`.br`, `.gz`) are served with their encoding.
- Scripts: your own files, or these CDNs only: https://cdn.jsdelivr.net, https://unpkg.com, https://cdnjs.cloudflare.com. Anything else fails the upload check; bundle it instead.
- Network: the build may call the Mmorada API and the origins you declare on the portal (Ajustes → "Orígenes permitidos"). Nothing else: the runtime sets a Content-Security-Policy per build.
- The game runs inside an iframe with `fullscreen`, `gamepad`, `autoplay` and `cross-origin-isolated` allowed (`COEP: credentialless`, so engines with threads work). Sound needs a user gesture, like anywhere on the web.
- Framework-agnostic: Phaser, PixiJS, three.js, Babylon, Construct, Godot (web export), Unity (WebGL export), plain canvas: anything that exports to a static folder. Keep the original project; only the exported build goes up.
- For the Library (the curated front): about two hours of game or a loop that holds twenty runs, finished art, UI and sound, 60 fps on integrated graphics, fullscreen, keyboard and gamepad, a cloud save, and a page with capsule, screenshots, trailer and controls. The team plays at least 60 minutes before opening it, within 5 business days. Before that the game lives as a playtest (the developer, the team and invited testers) or as a community app (a page, builds only the developer sees).

## The SDK

The build loads it from the runtime (always current, no install):

```html
<script src="/sdk/v1.js"></script>
<script>
  Mmorada.init().then(async (ctx) => {
    // ctx: { appId, versionId, channel, lang, user | null, scopes, dev }
    const save = ctx.user ? await Mmorada.saves.get("main") : null;
    start(save?.data ?? newGame(), ctx.lang);
  });
</script>
```

The types are at https://www.mmorada.com/dev/sdk.d.ts; read them before writing calls. Outside the platform (your dev server, a file) the SDK runs in **dev mode**: a local player named `dev`, saves in `localStorage`, purchases granted at once, rooms between tabs, nothing sent anywhere. The same code runs in both places, so develop and test locally, then upload.

- `Mmorada.init(options?)` → `Promise<Context>`. `Mmorada.user` is the player or `null`; `Mmorada.signIn()` asks the platform to sign them in and comes back to the game. Library games can be played signed out: ask for the account only when it matters (saving, buying, ranking).
- `Mmorada.profile()`: the player's avatar, frame and title (scope `profile:read`).
- `Mmorada.saves.get(slot?)`, `.set(slot, data, { ifVersion? })`, `.delete(slot)`, `.list()` (scope `saves`): 10 slots of 1 MB of JSON each. `ifVersion` refuses to overwrite a newer save from another device (`MmoradaError` with `code: "conflict"` and `current`).
- `Mmorada.achievements.unlock(key)` → `{ unlocked, unlockedAt, achievement }` (scope `achievements:write`), `.list()` (signed out too). Up to 100 achievements, defined on the portal (Juego tab) with key, name, description, an optional 128 × 128 icon, hidden or not.
- `Mmorada.leaderboards.submit(key, score)` → `{ best, improved, rank, period }` (scope `leaderboards:write`), `.top(key, { period?, limit? })`, `.list()`. Up to 20 tables (higher or lower wins; all-time, weekly or daily; a number or a time), defined on the portal. Integer scores; the player's best per period is kept.
- `Mmorada.competitive.get({ period?, scope?, limit? })` (no scope, signed out too): Mmorada's competitive standard as this game sees it (see "Competitive play" below). `board` (the ranking: `period` day, week (default), month or all; `scope` "game" (default) or "all"), `season`, and `me` when signed in (level, place, firsts, and per ranked mode: level, matches this season, how the last season ended).
- `Mmorada.presence.set(detail)` (scope `presence:write`): "playing <your game> · <detail>" on the player's profile while the tab is open; at most 60 characters, `""` clears it.
- `Mmorada.store.buy(key)` → `Promise<Purchase>` (scope `store`): the platform's purchase screen opens over the game and resolves with `state: "confirmed" | "cancelled"`. `key` is an item defined on the portal (Tienda tab, up to 100) or `"app"` for the game itself. `.list()`, `.entitlements()`, `.owns(key)`, `.consume(key, n?)`. The game never handles money: no prices of its own, no card forms, no third-party payment SDKs.
- `Mmorada.rooms` (scope `rooms`): `create(mode)` (a room with a code), `join(code)`, `find(mode, { onWaiting })` (matchmaking near the player's level), `rooms.invited`. **Shared rooms** (up to 50 players; the platform relays a shared object, each player's own data and messages; the game decides) or **rules rooms** (up to 16; the build's `server.js` runs on the platform with no network, each player sees their own `view`; ticks up to 20 Hz, places at the end, levels, ghosts for asynchronous runs). Up to 6 modes per game, declared on the portal (Multijugador tab). A game may also run its own servers (declare an allocator; up to 100 players a match).
- `Mmorada.feedback({ kind?, context? })`: opens the platform's feedback screen; what the player writes reaches the portal's Opiniones tab with the version and your `context` (2 KB). Put it behind a key or a pause-menu button.
- `Mmorada.reportError(error)`: uncaught errors are reported by themselves to the portal, grouped per version.
- `Mmorada.getToken()`: the game session token (a JWT, `aud` = your app id) for a backend of your own; verify it against `https://account.mmorada.com/.well-known/jwks.json`. A backend may also act for a player it verified through `/api/v1/apps/<appId>/server/*` (saves, entitlements, consumables, match results).
- `Mmorada.on("user" | "token" | "session-ended", handler)` → unsubscribe. On `session-ended`, stop writing and tell the player.

Scopes (`openid` and `profile` always; the rest asked on the portal, Ajustes → Permisos, and granted by the player on a consent screen the first time): `saves`, `profile:read`, `achievements:write`, `leaderboards:write`, `presence:write`, `store`, `rooms`. Ask only for what the game uses.

Full reference: https://www.mmorada.com/dev/sdk.md (the SDK's README) and https://www.mmorada.com/dev/sdk.d.ts (its TypeScript declarations). A game that cannot use the SDK can speak the postMessage protocol itself: https://www.mmorada.com/dev/protocol.md.

## Starting a new game (nothing to install)

A Mmorada game is a folder. The smallest one that already does what the platform expects:

```
my-game/
  index.html      loads /sdk/v1.js before src/game.js
  src/game.js     await Mmorada.init(), then the game
  style.css
  dev.mjs         a static server for local play (below)
```

```html
<!doctype html>
<html lang="es">
  <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Mi juego</title><link rel="stylesheet" href="style.css" /></head>
  <body>
    <main id="game"></main>
    <script src="/sdk/v1.js"></script>
    <script type="module" src="src/game.js"></script>
  </body>
</html>
```

```js
// src/game.js
const ctx = await Mmorada.init(); // { appId, versionId, channel, lang, user | null, scopes, dev }
const save = ctx.user && ctx.scopes.includes("saves") ? await Mmorada.saves.get("main").catch(() => null) : null;
start(save?.data ?? newGame(), ctx.lang.startsWith("en") ? "en" : "es"); // ctx.lang is a BCP 47 tag ("es", "en", "pt-BR"…)
// Save with a version so two devices never overwrite each other:
// const meta = await Mmorada.saves.set("main", state, { ifVersion: save?.version ?? null });
```

`/sdk/v1.js` is served by the platform next to every build; it is never part of the upload. For local play, serve the folder with any static server that answers `/sdk/v1.js` with the SDK: fetch it from `https://play.mmorada.app/sdk/v1.js` (public, CORS open) or proxy that path. A `dev.mjs` that does it, with nothing to install:

```js
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import http from "node:http";
import { extname, resolve } from "node:path";
const root = resolve(".");
const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", ".ogg": "audio/ogg", ".mp3": "audio/mpeg", ".wasm": "application/wasm", ".woff2": "font/woff2" };
let sdk = null;
http.createServer(async (req, res) => {
  let path = decodeURIComponent(new URL(req.url, "http://x").pathname);
  if (path === "/sdk/v1.js") {
    sdk ??= await fetch("https://play.mmorada.app/sdk/v1.js").then((r) => r.text());
    return res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8" }).end(sdk);
  }
  if (path.endsWith("/")) path += "index.html";
  const file = resolve(root, `.${path}`);
  try {
    if (!file.startsWith(root) || !(await stat(file)).isFile()) throw new Error();
    res.writeHead(200, { "Content-Type": TYPES[extname(file)] ?? "application/octet-stream", "Cache-Control": "no-store" });
    createReadStream(file).pipe(res);
  } catch {
    res.writeHead(404).end("Not found");
  }
}).listen(5173, () => console.log("http://localhost:5173 (dev mode)"));
```

Open http://localhost:5173: the SDK sees no platform and runs in dev mode (a local player, saves in `localStorage`, purchases granted, rooms between tabs), so the whole game can be played and tested here. The upload is the folder itself (zip it with `index.html` at the root; leave `dev.mjs` and `node_modules` out). An agent working for a person should also leave an `AGENTS.md` in the folder that points here, so the next session starts informed.

## Competitive play (the same for every game)

Mmorada has one ranking across all its games and one competitive history per player that never resets. A game joins it by using what it already has, with no code of its own:

- **Achievements** give ranking points by how rare they are among the game's players when unlocked (10 common, 25 rare, 50 epic, 100 legendary, once the game has 20 players). Design a ladder: easy ones for everyone, a few that almost nobody gets.
- **Weekly tables** (a leaderboard with period "week") give points to the week's first places (100 / 60 / 40, then 20 down to 10th) when 5 or more played.
- **Ranked modes** (multiplayer modes marked ranked) play seasons that follow the calendar's quarters: with 10 ranked matches a player qualifies, and at the end gets a place and a tier (champion, top 10, elite) kept on their profile for good, plus ranking points; levels then reset softly.
- Only games in the library count, and nobody earns points in a game they make.

`Mmorada.competitive.get()` lets the game show where the player stands (their level, their place this week, their ranked season). Put it on a results screen or in a menu; never build a parallel ranking or a season system of your own.

## Porting checklist (for a coding agent)

Do these in order. Keep the original project intact; produce an exported build that passes the checks above.

1. **Make it static.** Export the game to a folder with `index.html` at the root. Remove server code, build-time-only files and source maps. Paths relative and case-exact. Bundle any script that is not on the allowed CDNs. Measure the first screen: under 50 MB before anything is playable; lazy-load the rest.
2. **Add the SDK.** `<script src="/sdk/v1.js"></script>` before the game's scripts (or bundle `@mmorada/sdk`). Call `Mmorada.init()` first; start the game from its promise. In dev mode nothing changes for local testing.
3. **Identity.** Replace any login, guest name or player id of your own with `Mmorada.user` (`sub`, `handle`, avatar through `profile()`). Keep the game playable signed out when it can be; call `Mmorada.signIn()` when the player does something that needs an account.
4. **Saves.** Replace `localStorage`/IndexedDB saves with `Mmorada.saves` (JSON, 1 MB per slot, 10 slots). Handle `conflict` on `set` (show both or keep the newer). Keep a local fallback for signed-out play if the game allows it.
5. **Achievements and leaderboards.** List the game's achievements and tables (key, name, description, icon; higher/lower, period, number/time) so the person can create them on the portal, and wire `unlock(key)` / `submit(key, score)` with the same keys. A weekly table and a ladder of achievements (some rare) put the game in Mmorada's ranking; `Mmorada.competitive.get()` shows the player where they stand.
6. **Presence.** `Mmorada.presence.set("Nivel 3")` at natural moments; short, in Spanish.
7. **Store (only if the game sells).** Remove every payment path of your own. List items (key, name, price, consumable or not) for the portal's Tienda tab; use `Mmorada.store.buy(key)` and `.owns(key)` / `.consume(key)`. Nothing sold may give an advantage over other players; no loot boxes.
8. **Multiplayer (only if the game has it).** Replace your networking with `Mmorada.rooms`: a shared room when the clients can be trusted (co-op, party games), rules on the server (`server.js`) when they cannot (hidden information, ranked). Declare the modes for the portal (name, players, tick, ranked or not).
9. **Language and screen.** Spanish text first (`ctx.lang` for others), fullscreen that works (the iframe allows it), keyboard and gamepad, touch controls if the game can be played on a phone. Sound after a user gesture.
10. **Errors and feedback.** Leave `reportError` to the SDK; add `Mmorada.feedback()` behind a key or a pause-menu button. On `session-ended`, stop and tell the player.
11. **Test locally**: open the build with a static server that answers `/sdk/v1.js` (the `dev.mjs` above); the SDK's dev mode plays the whole thing (saves, purchases, rooms between tabs). Then write the person a short handover: what to create on the portal (achievements, leaderboards, items, modes, scopes, origins), the capsule (616 × 353) and screenshots (1280 × 720) they need, and the zip to upload.

## On the portal (the person does this, in Spanish)

1. https://www.mmorada.com/dev/new: register the app with a name and an address (`/apps/<slug>`). It starts as a community app.
2. Ajustes: description, controls, genres (up to three), capsule 616 × 353, screenshots 1280 × 720, trailer, the scopes the game asks for, the origins it may call, a planned release date if any.
3. Versiones: upload the build as a zip. The browser unzips and checks it (entry page, paths, sizes, external scripts, case clashes, source maps) and uploads file by file. A ready version is immutable: the testing channel moves to it at once; publish it to stable when it is right, or roll back. The newest 5 versions are kept.
4. Testers: invite people by link; the game becomes a playtest they can open.
5. Juego: achievements and leaderboards. Tienda: the price and the items. Multijugador: the modes.
6. Ask for the review from the portal: the team plays it and opens the Library, or says what is missing.
7. Ingresos: the payout method and the tax form (W-8BEN; Mmorada pays no US persons for now). Payouts monthly, 30 days after the sale, from US$100.

## Where things are

- Library: https://www.mmorada.com/apps · a game's page: https://www.mmorada.com/apps/<slug> · where it is played: https://www.mmorada.com/play/<slug>
- Developer portal: https://www.mmorada.com/dev · earnings: https://www.mmorada.com/dev/ingresos
- The SDK: https://www.mmorada.com/dev/sdk.md (README), https://www.mmorada.com/dev/sdk.d.ts (types), https://www.mmorada.com/dev/protocol.md (host ↔ game protocol); the script itself is `/sdk/v1.js` next to every build.
- The site for readers and crawlers: https://www.mmorada.com/llms.txt · Terms: https://www.mmorada.com/terms · Privacy: https://www.mmorada.com/privacy
- Questions: contacto@mmorada.com, or the community "Juegos en Mmorada" at https://www.mmorada.com/c/juegos-en-mmorada.
