CLAWCADE Insert coin ▸
▸ Docs

Build a game for Clawcade.

Everything you need to build, publish, and level-up a single-file HTML game.

1What a Clawcade game is

A Clawcade game is one self-contained HTML file. All your HTML, CSS, JavaScript, and assets live inside that single file — inline <style> and <script>, images as data URLs or drawn on a <canvas>. No build step, no folder of files, no server.

You publish that file, Clawcade hosts it, and you get a permanent play link like play.clawcade.gg/g/<id> you can share anywhere.

Keep it to one file and it will "just work." A bundler build with separate files (Phaser, Three.js, Vite…) goes up as a .zip instead (see below).

2Publishing

Two ways:

  • Web: go to /publish, drag your .html file in (or paste the HTML), and hit Publish. You get a play link and a one-time edit key — save it, it's the only way to edit the game later.
  • From your AI: add the Clawcade MCP connector and just ask it to publish — see section 10.

New games start unlisted — playable by anyone with the link, but not shown in the public arcade until you submit them (section 9).

Multi-file games (static ZIP bundles)

Built with Vite, React, Phaser, Three.js, Unity or Godot — i.e. an index.html plus separate /assets/*.js|css|png|wasm? Don't inline it. Zip your build and upload the .zip — Clawcade hosts the files as-is and relative paths just work.

  • index.html must be at the zip root (one wrapping folder is auto-stripped).
  • No build runs on our side — upload an already-built static site (your dist/).
  • Limits: 20 MB zip, 60 MB uncompressed, 200 files. Allowed types include .wasm and common assets; .svg isn't allowed yet.
  • Web: pick the .zip on /publish. AI: the get_bundle_upload_url tool (zip -r game.zip dist/ then curl).

The injected Clawcade APIs (leaderboard, multiplayer, mobile controls) work in bundles too.

3The sandbox & rules

Your game runs inside <iframe sandbox="allow-scripts allow-pointer-lock"> on a separate origin from the rest of Clawcade. That keeps everyone safe, and it means a couple of things for you:

  • One file only. External URLs may be blocked or slow; inline everything you can.
  • Saving progress: just use localStorage. Real storage is blocked in the sandbox, so Clawcade swaps in its own localStorage that persists per game on the player's device: reads work at startup, writes are saved automatically, and it survives reloads and new versions of your game. No server needed. Limits: strings only (use JSON.stringify), about 500 KB per game, and it stays on that device and browser (Safari may clear it after ~7 days without a visit). Players get a Delete my save button. sessionStorage resets each visit. Don't use window.name; Clawcade uses it to hand your game its save.
  • No login / no cookies. Players are anonymous; saves belong to the device, not an account.
  • Talk to Clawcade via postMessage (for leaderboards) or the injected Clawcade object (for multiplayer). Both are described below.

4Leaderboards

Any game can have a leaderboard with one line of code. When the player's score or result is final, post it to the page:

// N is a number. name is optional (the page will ask if you omit it).
  parent.postMessage({ clawcade: 'score', score: N, name: 'optional' }, '*');

Then name the metric when you publish or edit — e.g. "Max round", "Best time", "Max jumps" — and choose the order:

  • high (default) — bigger is better (points, distance).
  • low — smaller is better (time, strokes).

Clawcade shows a top-10 panel under the game (best score per name). Scores are client-reported and anonymous — fun, not cheat-proof.

5Multiplayer

Clawcade gives every game a global Clawcade object for invite-code lobbies: one player hosts a room and gets a short code, friends join with it, and you exchange messages. There is no server for you to run — Clawcade relays the messages for you.

Mental model. Clawcade is a relay: it passes JSON messages between everyone in a room and tells you who's there. It does not run your game logic. The first player to enter the room is the "host" — make the host the authority (it owns the real game state) and have everyone else send input and render what the host broadcasts. This one pattern handles both turn-based and real-time games.

The whole API

CallWhat it does
await Clawcade.host()Open a new room. Resolves to { code, id, isHost:true } — show code to a friend.
await Clawcade.join(code)Join an existing room. Resolves to { id, isHost:false }, or rejects if the code is wrong/full.
Clawcade.send(obj)Broadcast any JSON obj to everyone else in the room.
Clawcade.on(event, cb)Listen for an event (see below).
Clawcade.leave()Leave the room.
Clawcade.invitedRoomThe room code from a shared #room=… link, or null.

Events you can listen for

EventCallback getsMeaning
'message'{ from, data }Another player sent data (what they passed to send()); from is their id.
'peers'[ {id, host}, … ]The roster changed (someone joined or left). Use its length to know when to start.
'host'—You were promoted to host (the host left, or didn't come back within 8 s).
'open'{ peerId, isHost, code }Your connection is live (also after every reconnect). Set your role from isHost: it can change while you were away.
'close'—You left, the room is full or doesn't exist, or a dropped connection couldn't get back in within 2 minutes.
'error'{ error }Hosting or joining failed, or a message was too big (each send() must be ≤ 8 KB of JSON).
'invite'codeThe player arrived via a shared link carrying a room code — auto-join with it.
'reconnecting'{ attempt }The connection dropped and Clawcade is getting you back into the same room (show a "reconnecting…" hint).
'reconnected'{ peerId, isHost, code }Back in, with the same Clawcade.playerId. The host should re-send the full state.

A minimal example

// --- One player hosts ---
  const room = await Clawcade.host();   // { code:'ABCD', id, isHost:true }
  showCodeToFriend(room.code);

  // --- The friend joins (from their screen) ---
  await Clawcade.join('ABCD');

  // --- Start once both players are present ---
  Clawcade.on('peers', function (list) {
    if (list.length === 2) startGame();
  });

  // --- Receive messages from the other player ---
  Clawcade.on('message', function (m) {
    // m.from = sender id, m.data = whatever they sent
    applyUpdate(m.data);
  });

  // --- Send to everyone else ---
  Clawcade.send({ paddleY: myPaddle.y });

The host-authoritative pattern (recommended)

For anything with shared state (a ball, a board, scores), don't let each side simulate independently — they'll drift. Instead:

  1. The host runs the real simulation each tick and broadcasts the state: Clawcade.send({ ball, scores }).
  2. Guests send only their input (Clawcade.send({ paddleY })) and render whatever the host last sent.
  3. If you get a 'host' event, you just became the authority — start simulating.

Smooth movement (interpolation) · required for real-time games

Any real-time multiplayer game needs this. Without it, other players and enemies stutter and teleport. Only strictly turn-based games (chess, cards) can skip it. Clawcade warns at publish time if a multiplayer game never calls Clawcade.smoothing().

State arrives in bursts (~20–30 times a second), so drawing the newest snapshot directly looks steppy. The built-in interpolator smooths positions — render a hair in the past (default 100 ms) and lerp between the two surrounding snapshots.

Four rules — get these wrong and entities (enemies, other players) flicker or vanish.
  1. Positions, not existence. Decide which entities exist from the latest message; use the buffer only to smooth each one's x/y. Don't ask get() whether something exists.
  2. One shape per id. get() returns the newest snapshot's fields, so if you push two shapes into one id (a player update and an enemy list), the last one wins and the other's fields disappear that frame. Give each thing its own id.
  3. get(id) returns null until that id has a snapshot — null-check it. Put requestAnimationFrame at the top of your loop so one thrown frame can't kill rendering.
  4. Host sends full state each tick (idempotent), not one-off "spawn" deltas — a dropped message then self-heals on the next tick instead of leaving a permanent hole.
var enemyBuf = Clawcade.smoothing({ delay: 100, angles: ['angle'] });

  // HOST, every tick: broadcast the WHOLE list, each with a stable id
  Clawcade.send({ t: 'state', enemies: enemies.map(e => ({ id: e.id, x: e.x, y: e.y, angle: e.a })) });

  // GUEST: existence from the message, positions from the buffer
  var liveIds = [];
  Clawcade.on('message', function (m) {
    if (!m.data || m.data.t !== 'state') return;
    liveIds = m.data.enemies.map(e => e.id);              // who exists right now
    m.data.enemies.forEach(e => enemyBuf.push(e.id, e)); // one buffer per id, one shape
  });

  // render loop:
  liveIds.forEach(function (id) {
    var s = enemyBuf.get(id);   // null until first snapshot — guard it
    if (s) drawEnemy(s.x, s.y, s.angle);
  });

A single moving thing (one opponent) can skip ids: buf.push(data) / buf.get(). Names in angles use shortest-path angular lerp (no spinning past π). Bare helpers Clawcade.lerp(a,b,t) / Clawcade.lerpAngle(a,b,t). Opt-in — only worth it for continuous movement.

Reconnection & keeping state

Phones drop connections all the time (a network switch, a screen-lock, a tunnel). Clawcade now reconnects automatically — you don't manage sockets. There's also a keepalive so dead connections are noticed fast. Each player has a stable id that survives the reconnect: Clawcade.playerId, which is also the from on their messages.

To keep a player's stuff (weapons, perks, score) across a reconnect, the reconnect alone isn't enough — your game has to do "session resume":
  1. Key every player by Clawcade.playerId — never by join order or a per-spawn id. The host keeps each player's state in a map under that id.
  2. On disconnect, don't kill them — hold their character for a grace period (~20s) instead of letting it die.
  3. On reconnect, re-attach them to that held character and resync their loadout, rather than spawning fresh.

Want a "reconnecting…" overlay on the guest? Listen for Clawcade.on('reconnecting', cb). (If a game has frozen entirely, the play page also shows its own always-responsive Reload/Exit buttons — that part is automatic.)

Sharing & auto-join

When a game hosts a room, the play-page URL gains #room=CODE. Share that link and the friend's game can auto-join:

Clawcade.on('invite', function (code) {
    if (code) Clawcade.join(code);
  });
Good to know. Up to 8 players per room. The host is trusted — it's a relay with no anti-cheat, so it's perfect for playing with friends, but don't use it for ranked/competitive integrity: for competitive real-time games use Clawcade Server (the clawcade_guide topic server). A working relay example ships as the CLAWPONG sample (host-authoritative 2-player Pong).

6Mobile controls

Clawcade can show an on-screen gamepad on phones and tablets and translate it into the same keyboard events your game already listens for. So a keyboard game becomes touch-playable with one line — or zero, if the player taps the 🎮 button.

If your game uses arrow keys (or WASD)

Just declare a D-pad. It maps the four directions to the arrow keys, so your existing keydown handlers work unchanged:

if (window.Clawcade) Clawcade.controls({ dpad: true });

Want WASD instead, or custom direction keys? Pass codes: Clawcade.controls({ dpad: { up:'KeyW', down:'KeyS', left:'KeyA', right:'KeyD' } }).

Add action buttons

List up to ten buttons; each maps to a key. Set dpad:false if you only need buttons:

Clawcade.controls({
    dpad: true,
    buttons: [
      { label: 'JUMP', key: 'Space' },
      { label: 'FIRE', key: 'KeyX' }
    ]
  });

Key names are standard code values: 'ArrowUp', 'Space', 'Enter', 'KeyA'…'KeyZ'.

If your game doesn't use the keyboard at all

Handle presses directly — you get { id, label, key, phase } where phase is 'down' or 'up'. You can also poll a button any frame with Clawcade.pressed('fire'):

Clawcade.onButton(function (b) {
    if (b.id === 'a' && b.phase === 'down') jump();
  });

Analog joysticks (twin-stick / aim)

Discrete keys can't express "aim 37° that way." For movement + free aiming (top-down shooters, twin-stick), declare analog sticks — Clawcade renders them; you read their values each frame:

// left stick = move, right stick = aim
  Clawcade.controls({ move: 'joystick', aim: 'joystick' });

  // then in your game loop:
  var mv = Clawcade.stick('move');   // { x, y, angle, magnitude, active }
  player.x += mv.x * speed * dt;       // x,y are -1..1 (y is +down, like canvas)
  player.y += mv.y * speed * dt;

  var aim = Clawcade.stick('aim');
  if (aim.active) { facing = aim.angle; shoot(facing); }   // auto-fire while aiming

stick(id) returns { x, y, angle, magnitude, active }: x/y are −1…1, angle is in radians (0 = right, increasing clockwise — drop it straight into Math.cos/Math.sin), magnitude is 0…1, and active is whether it's being held (when released, angle keeps the last facing). You can mix sticks, a D-pad, and buttons in one controls() call.

This is the fix for top-down shooters. On desktop you probably aim with the mouse; on touch, read Clawcade.stick('aim') instead. The Claw Survivor sample is a full twin-stick example you can copy.

The joysticks are floating & Brawl-Stars-style: each takes a whole half of the screen, appears under the thumb, and the aim angle keeps tracking your finger even outside the ring (small dead zone at the center). No game code needed — it's all behind the same stick() API.

Play mode is a toggle (🎮, next to the page's menu tab, top-center by default), OFF by default. The full-screen joysticks cover half the screen each, so they'd block your game's own menu buttons if always on. Instead, touch players see a 🎮 button: tap it to enter play mode (controls appear, screen fills, page scrolling locks so you don't drift); tap ✕ to leave. So your menus stay tappable. If your game has its own menu/game-over screens, you can also call Clawcade.showControls(false) when you show a menu and Clawcade.showControls(true) when play starts.
Two things to know. The pad shows only on touch devices — desktop players never see it. And if your game already has its own touch controls (like swipes or tap-to-move), turn the pad off with Clawcade.controls(false) so it doesn't get in the way.

Even without any of this, touch players get a 🎮 toggle that summons a default arrows + A/B pad — so most keyboard games on Clawcade are already iPad-friendly. Reference: the Claw Snake sample (arrow-key game, one line of controls()).

7Cover images

Add a cover image so your game looks good in the arcade and in link previews. On /publish (or /edit) pick an image — png, jpeg, webp, or gif, up to 2 MB. From your AI, use the get_thumbnail_upload_url tool (it needs the game's edit key).

8Editing & the recheck

Every game has a secret edit key (shown once when you publish). With it you can change the HTML, title, leaderboard, or cover at the /edit page — the play link never changes.

Public games go through a recheck. If your game is not listed in the arcade (the default), edits go live immediately. But once a game is public, an edit does not change the live game right away — it's saved as a pending update and a moderator rechecks it before it replaces the live version. Your players keep seeing the current version until then, and the link stays the same.

When you edit a public game, add a short "what changed" note (a changelog, up to 600 characters). The reviewer sees it, which makes approvals faster. In the API/MCP this is the description field.

9Getting into the arcade

Publishing keeps a game unlisted (link-only). To appear on the public arcade homepage, submit it for review — the button is on your publish result and the /me dashboard (from your AI, the submit_to_arcade tool). A moderator approves it, then it's listed. It stays playable by its link the whole time.

10Publishing from your AI

Any AI can make a Clawcade game. How it publishes depends on what your AI can do:

  • Apps with connectors (MCP): Claude, ChatGPT (Plus and up, developer mode), Codex, Cursor, VS Code Copilot, Gemini CLI, Windsurf/Devin, Cline, Le Chat, Zed. Add the remote server https://clawcade.gg/mcp (Streamable HTTP, no auth, no API key; legacy SSE at /sse). Then just say "make a game and publish it to Clawcade." Exact steps for each app are on /publish.
  • Agents with a terminal but no connector: tell them to read clawcade.gg/llms.txt. It has the full HTTP API (publish, edit, cover, submit, status) as curl commands, plus all the in-game APIs below.
  • Plain chat AIs (free ChatGPT, Gemini, Copilot, Grok…): ask for the game as one self-contained HTML file, then paste it on /publish.
# Claude Code
claude mcp add --transport http --scope user clawcade https://clawcade.gg/mcp
# OpenAI Codex
codex mcp add clawcade --url https://clawcade.gg/mcp
# Gemini CLI
gemini mcp add --transport http clawcade https://clawcade.gg/mcp
ToolWhat it does
clawcade_guideThe in-game API reference (leaderboards, multiplayer, touch controls, smoothing) plus a pre-publish checklist.
publish_gamePublish a small single-file game inline. Returns the link and the one-time edit key.
get_upload_urlUpload URL for a large HTML build; the agent curls the file so it never passes through the model.
get_bundle_upload_urlUpload URL for a multi-file .zip build. Pass slug + edit_key to replace an existing bundle.
edit_gameChange the build, title or leaderboard (needs the edit key).
get_thumbnail_upload_urlUpload URL for a cover image (needs the edit key).
submit_to_arcadeRequest public listing; a moderator approves it.
get_gameStatus, plays, likes and top scores for any game. Read-only.

Questions or a broken game? Use the Report this game button under any game. Happy building.