← Back to the arcade

Put your game
on the floor

What to build, how to hand it over, and how to use the accounts and leaderboards without writing a backend.

Contributor kit · last revised 2026-08-16

The short version

Build a game that runs in a browser with no install. Zip it. Send it. It gets played, and if it clears the bar it gets a cabinet, a URL and — if you want them — accounts, saves and leaderboards for free.

You do not need to write a backend, run a server, handle passwords, or know anything about how any of this is hosted.


1. What it has to be

Four rules, checked in this order. The first one you fail is the answer.

Nothing that picks a pocket

No ads, no battle pass, no lootboxes, no “wait four hours or pay $2.99”. If your game has a monetisation designer it has one designer too many.

Nothing phoning home

No analytics, no ad pixels, no scraping location or contacts, no quietly building a list to sell on. If your game wants to tell somebody about the extended warranty on a car I do not own, don’t bother.

And yes — submissions get opened with the network tab up. Every request a game makes gets looked at, and anything talking to a host that is not this one had better have an obvious reason. If it is having a side conversation with somebody, we will see it.

This is not paranoia about telemetry in general: if your game genuinely needs to call something external, say so up front and it is probably fine. The rule is against doing it quietly.

You made it

You, some friends, an AI you argued with at 2am — all fine. A studio with a legal department is not, because the moment one of those turns up some indie gets a letter about a word they used.

Still breathing

Actively developed. Not finished — nothing here is finished — but not abandoned in 2019 either.

Have a mouth on it

Attitude encouraged, snark strongly encouraged. Corporate voice is the one thing that will actually get you turned down.

And technically


2. What to send

A zip, laid out like this:

your-game.zip
├── game.json          <- the manifest. Required.
├── version.json       <- build stamp. Required, regenerated per release.
├── index.html         <- entry point. Required.
├── roadmap.md         <- optional, shows up under /docs
├── assets/            <- whatever you like in here
└── ...

Everything except game.json is yours to arrange however you want. Nothing rewrites your files; the zip is unpacked and served as-is.

game.json

{
  "id": "star-catcher",
  "title": "STAR CATCHER",
  "kind": "Arcade · Single player",
  "blurb": "Catch the falling stars. Miss three and it is over.",
  "accent": "#4de0c0",
  "author": "Your Name",
  "url": "https://wherever-you-develop-it",

  "accounts": true,
  "leaderboard": {
    "enabled": true,
    "perSeed": false,
    "scoreLabel": "POINTS"
  },
  "save": {
    "enabled": true
  }
}
FieldWhat it does
idYour slug. Lowercase, hyphens. Becomes your URL (/star-catcher/) and your key in the database. Pick it once — changing it later orphans every save and score.
titleWhat appears on the cabinet marquee.
kindThe small line above the title. Genre, player count, whatever.
blurbTwo or three sentences on the card. Write it like a person.
accentYour cabinet's colour. The portal has no colour of its own — every hue on that page belongs to a game, so this is genuinely yours.
accountsWhether you want sign-in at all. A game can be entirely anonymous.
leaderboard.perSeedTrue if your board is per-world rather than global. Most games want false.
save.enabledWhether you want cloud saves.
On persistence fields: you do not declare them. The server stores your save as one opaque JSON document and never looks inside it, so its shape is entirely yours and you can change it whenever you like without telling anybody. That is deliberate — a schema that modelled every game's save would need migrating every time any game changed a field.
Version your own document ({ "v": 2, ... }) and migrate it on load. That is what Rustlands does.

version.json

Published at your game’s root and regenerated on every release. The portal reads it to put a build stamp on your cabinet, so a visitor can see at a glance whether the thing has been touched this year.

{
  "version":  "1.04.000",
  "released": "2026-08-16T23:39:45Z",
  "commit":   "62818f6",
  "release":  "optional; whatever your pipeline calls this build"
}
FieldWhat it does
versionX.YY.ZZZ, zero-padded. Required.
releasedISO-8601 UTC. When this build was cut.
commitShort git hash, if you use git. Optional but useful when somebody reports a bug.
releaseFree text. Ignored by the portal.
Why the zero padding. 1.02.000 sorts lexically the same way it sorts semantically. Unpadded, 1.13.0 lands before 1.2.0 in sort, in a filename listing, in a database ORDER BY and in every ad-hoc script anybody writes later — and each of those then needs a version-aware comparator to work around a problem that three zeroes would have prevented. Pad the minor to two and the patch to three.

Keep a VERSION file with just the number in it and have your release script read it, rather than typing the version into two places and eventually disagreeing with yourself.


Building it with an AI session? Point it at adding-your-game.md — the same contract written for a machine to follow, with the schemas, the deploy handshake and the things that fail silently. Fetching that beats pasting this page into a prompt.

3. The hook

One file, no build step, no dependencies. Served from the arcade so there is exactly one copy — do not vendor it, or you will silently keep running against an endpoint that has moved on.

<script type="module">
  import { Arcade } from '/arcade.js';

  const arcade = new Arcade('star-catcher');   // your id from game.json

  // Who is playing? null means nobody, or offline, or the server is down —
  // all of which mean the same thing to you.
  const me = await arcade.me();
  if (me) console.log(`Playing as ${me.displayName}`);

  // Cloud save. The document is yours; the server never opens it.
  await arcade.save({ v: 1, level: 4, coins: 120 }, revision);
  const { document, revision } = await arcade.load();

  // Leaderboard.
  await arcade.submitScore(9100, { level: 4, time: 83 });
  const board = await arcade.leaderboard({ limit: 10 });
  // -> [{ name: 'Rusty Joe', score: 9100, detail: {...}, at: '...' }]
</script>

Things worth knowing before you use it

Full surface

CallReturns
me()player, or null
providers()which sign-in methods exist
setName(name)rename in your game only
setName(name, {global:true})rename account-wide
clearName()fall back to the account default
signOut()—
load(){ document, revision }
save(doc, revision){ ok, revision } or { ok:false, conflict }
submitScore(score, detail, seed)boolean
leaderboard({seed, limit})entries, highest first

4. Multiplayer

If your game wants it, there is a signalling server that introduces two browsers to each other and then gets out of the way. Game traffic goes peer-to-peer over WebRTC — the server never sees a packet of it, which is why one small box can host a lot of players.

Open a room and you get a five-character code. Somebody types that code and the two of you are connected. There is a relay fallback for players whose network defeats direct connection.

This part is less finished than the rest. The transport and wire format are built and tested and Rustlands is the first game wiring them to actual gameplay. If you want multiplayer, talk to me — the hook exists but it has not yet been used by anybody who did not write it, which is the only real test of an API.

5. Sending it

Zip it and send it, along with anything you want me to know. It gets played before it gets a marquee — that is the whole review process, and it is done by a person, not a form.

If you would rather not zip things, a git repo is fine too. If your game updates often, a repo is better, because then updating your cabinet is a pull rather than an email.

Your roadmap.md, if you include one, appears under /docs alongside everyone else's.

← Back to the arcade