antics.gg
21 games, 0 developers · 186,412 plays · 0 knows what they're doing

YOUR GAME WORKS · IT'S JUST LONELY

Add multiplayer to your game.

Straight up: multiplayer is not a feature you add to a file. It's a second program — one that has to be awake at 11pm when your friend finally clicks the link, hold the room, decide whose version of the world is real, and not fall over when the host rage-quits. Your agent can write that program. It cannot be it: no always-on machine, no domain, no certificate. We already are one. So the part you can't vibe-code is the part you don't have to.

Or do it by hand ↓

ANY BROWSER GAME · NO ENGINE, NO REWRITE · FREE TO START, NO ACCOUNT · YOUR FRIENDS JOIN BY TAPPING A LINK.

claude code — in your game's folder
› claude mcp add antics -- npx -y antics-mcp
Added MCP server antics
› add multiplayer to this game with antics and ship it
reading the antics docs — the whole SDK in one file
patching game.js — +4 lines
deploying — 1 file, 31 KB
checking it — 2 players, 1 room, positions agree
Done. Send your friends: antics.gg/r/KX7P2M
claude mcp add antics -- npx -y antics-mcp
THE PART THE TUTORIAL SKIPS

"Just add multiplayer" is four words and a whole backend.

Every other feature you've ever shipped lived inside your codebase. This one doesn't — which is exactly why it's the request that quietly kills a finished game.

The socket server is the small part

Opening a WebSocket is an afternoon. What follows is not: room codes that don't collide, matchmaking a second player into the right room, heartbeats, backoff, an origin policy, a TLS certificate, and a process that is awake at 11pm when your friend finally clicks the link. localhost is not a place other people can go.

State sync is where it gets weird

Two browsers will disagree, so something has to be right. Who owns the world? What happens to a player who joins in round three — do they get a delta they can't apply, or the whole world? What happens when the owner leaves? Getting these wrong doesn't crash the game; it makes it subtly, unfixably weird.

And then it has to stay up. Forever.

Multiplayer isn't done when it works on your machine twice. Phones sleep, wifi drops, tabs get backgrounded, people rage-quit mid-round. Every one of those is a state machine somebody has to own — and if that somebody is you, congratulations: you now run infrastructure for a game you made for fun.

Your agent will happily write you a server. It just can't be one.

Ask any coding agent for multiplayer and you'll get something plausible in about ninety seconds — a ws server, a rooms Map, a heartbeat, a reconnect loop. The code is usually fine. It's also running on your laptop, which is a place only you can go, and it stops existing the moment you shut the lid. No better prompt fixes that. The thing needs somewhere to live, awake, on a URL, and no model has a spare machine in its pocket. That half is ours — already built, already running, so nobody has to rediscover it at midnight.

import { joinRoom } from "/sdk/v1.js";
const room = await joinRoom({});   // everything above is now somebody else's job

The complete API is one file — antics.gg/llms.txt — small enough to read in a sitting, or to paste into an agent in one go.

THE WHOLE JOB · ONE PROMPT

Say it in English. Get a link.

The retrofit is small enough that an agent with the docs in context gets it right in one pass — the patch, the deploy, and a check that two players really do see each other. Your game keeps its rendering, its physics, its art and its bugs. It just stops being a single-player game.

MCP ONE COMMAND, THEN ONE SENTENCE

Give the agent hands

One line wires antics into Claude Code, Codex or Cursor. After that the agent can read the SDK, patch your game, call deploy_game and hand back a playable link — without you opening a dashboard or signing up for anything.

claude mcp add antics -- npx -y antics-mcp

# Codex (same server)
codex mcp add antics -- npx -y antics-mcp

Then, in your own words: "add multiplayer to this game with antics and deploy it". It can also run the deployed game headlessly with two players in one room and read the synced numbers back — so "the sync works" is a measurement, not a vibe.

NO MCP DOCS + CLI

Hand it the docs instead

Any agent that can read a file and write JavaScript works. Point it at antics.gg/llms.txt — the entire API in one file, written to be held in view at once — then ship the result from your terminal.

npx antics-cli deploy game.html      # single file
npx antics-cli deploy ./my-game      # whole project dir

Deploying needs no account. Sign in only when you want a link that outlives the day and a leaderboard that outlives the room.

See the MCP kit →

You want to know exactly what it put in your file, don't you. Every line, every API, every trap — it's all below. Go on then ↓

FINE. THE ACTUAL WIRING

Here's every line, by hand

Nobody needs this section — the prompt above does all of it. It's here because some people won't run a command they can't read the output of, and honestly: respect. Four steps, real API, copy-pasteable into a game that already runs.

game.js — the entire diff
+ import { joinRoom } from "/sdk/v1.js";
+ const room = await joinRoom({});
function frame(dt) {
update(dt); // your game, untouched
+ room.me.setState({ x: me.x, y: me.y });
+ for (const p of room.players) draw(p.state, p.name);
requestAnimationFrame(frame);
}
Four lines. Everyone on the link now sees everyone else.
  1. STEP 1

    Join a room

    When the game is hosted by antics this call takes no arguments: the SDK finds the server on the same origin and reads the room code out of the page's query string. Presence is immediate — the instant it resolves you are in room.players for everyone else, so there is no lobby protocol to build.

    import { joinRoom } from "/sdk/v1.js";
    
    const room = await joinRoom({});
    room.code      // "KX7P2M"
    room.link      // the URL that puts someone else in THIS room
    room.me        // you
    room.players   // everyone, including you
  2. STEP 2

    Publish each player's own slice

    Put room.me.setState wherever you already move the local player, and read everyone back from room.players where you already draw. Writes apply locally at once and the SDK coalesces them to about 20 Hz — call it every frame, don't throttle it yourself.

    room.me.setState({ x: me.x, y: me.y, angle: me.angle });
    
    for (const p of room.players) {
      const s = p.state;   // your own write is already applied locally
      drawPlayer(s.x, s.y, s.angle, p.name);
    }

    Each player writes only their own slice. That's not a style preference: route everyone's movement through one authority and your own controls start waiting on a network round trip, which feels like mud.

  3. STEP 3

    Give the shared world to the host

    One client is the host, and only that client may write room.setState — the phase, the wave, the NPCs, the scoreboard. Put the simulation inside room.onHostTick: it fires ~30 Hz only while you're host and moves to whoever is host next, so you never hand-roll the start/stop wiring. Everyone renders from onState, which delivers the full merged state, not a delta.

    room.onHostTick((dtMs) => {          // runs only while you're the host
      world.step(dtMs);
      room.setState({ phase: world.phase, wave: world.wave });
    });
    
    room.onState((state) => renderWorld(state));   // every client, host included
  4. STEP 4

    Send events for the things that happen once

    State answers "what is true right now". Events answer "what just happened" — a shot, a pickup, a hit. They're reliable and ordered, never coalesced, and a third argument replies to one player instead of the room.

    room.send("shoot", { x, y, angle });
    room.on("shoot", (payload, from) => spawnBullet(payload, room.player(from)));
    
    room.send("hit", { dmg: 5 }, from);   // reply to just that player

    Act on your own client first, then send. Waiting for another client to detect your hit and echo it back stacks two network hops onto your own trigger — the single most common reason a working integration still feels laggy.

THE SEVEN THINGS YOU'LL SEARCH FOR NEXT

Everything else multiplayer drags in

Two browsers agreeing is the start. These are the questions that arrive immediately afterwards — each one is a weekend if you're building the backend yourself, and a paragraph if you're not.

SEARCHED AS · "multiplayer room system javascript"

Rooms and join links

A room is created the first time somebody opens the game and joined by everyone who follows the link. You don't mint codes, store them, expire them or garbage-collect empty ones. room.code is the six-character code (no 0/O/1/I, so it survives being read aloud) and room.link is the URL that drops someone into this room.

The page around your game already renders the code, a copy button, a QR code and the live player count — so don't rebuild that chrome inside your canvas, it only duplicates what's above it.

API: room.code, room.link, room.players, room.onJoin, room.onLeave.

SEARCHED AS · "real-time state sync javascript game"

Shared state vs per-player state

There are exactly two surfaces, with different write rules the server enforces. room.state is the shared world and only the host writes it — phase, timers, NPCs, the scoreboard. room.me.setState() is each player's own slice: their position, their input, their hat.

Both are flat key/value maps merged last-writer-wins; a null value deletes a key, and nested objects are replaced wholesale rather than deep-merged. Both onState and onPlayerState hand you the full surface, so a late joiner renders correctly from the first callback — there is no delta to replay.

API: room.setState / room.onState, room.me.setState / room.onPlayerState.

SEARCHED AS · "host migration multiplayer game"

Host authority and host migration

The room creator is the host. If they leave, the relay re-elects the oldest remaining connection and fires onHostChange. Authority also moves without anyone leaving: a host that backgrounds its tab hands off so the shared simulation doesn't freeze on a locked phone. Treat room.isHost as a value that can flip under you at any moment, and never cache it.

The clean way to survive all of that is to never own the loop yourself: onHostTick starts when you become host and stops when you lose it. Writing shared state as a non-host throws NOT_HOST rather than silently diverging.

API: room.isHost, room.host, room.onHostChange, room.onHostTick.

SEARCHED AS · "websocket reconnect game state"

Reconnects, leavers and late joiners

The SDK reconnects on its own with jittered backoff and resumes the same player id within a 60-second grace window, so a tunnel or a lock screen doesn't cost somebody their identity mid-match. On resume the server sends the full state again and the SDK reconciles, emitting the join / leave / host changes that happened while you were away.

What stays yours is the game-design half: a disconnected player never reports their own death, so in any last-one-standing mode prune absent ids at a natural boundary — round intro, match start — or the person who rage-quit wins by doing nothing.

API: room.onJoin, room.onLeave, room.onError, room.ping.

SEARCHED AS · "multiplayer game leaderboard backend"

Leaderboards

One await, and you get back the player's rank and their best. Boards keep each player's highest score, so a resubmit never lowers it; for fewest-moves-style metrics submit an inverted score so "fewer" ranks higher. Submission is anti-spoof, not anti-cheat — a signed session token, a minimum session age of ten seconds, a maximum score and a rate cap. Rejections carry a code and a hint on the error object, so retry on the code rather than parsing text.

In a keyless room the board lives with the room. Deploy under a project and it persists, and room.game is non-null to tell you which you've got.

API: await room.submitScore(n), getLeaderboard({ game: room.game, limit: 10 }).

SEARCHED AS · "where to host a multiplayer html5 game"

Hosting and deploy

A deploy is one self-contained HTML file or a whole project — a path→content map with index.html as the entry, relative imports resolving against the bundle root, up to 3 MB per file and 12 MB total. No Docker, no pipeline, no domain, no certificate.

Which URL you share matters exactly once. /g/<hash> is pinned to the bytes you deployed — right for "play this build", wrong for a link you post, because a redeploy has a different hash. Deploy under a project and you also get /p/<slug>, which always mints a room on your latest build, plus /world/<slug>: one permanent shared room with a public page and a global board.

Ship it: curl -F [email protected] https://antics.gg/api/deploy — or npx antics-cli deploy game.html.

SEARCHED AS · "multiplayer browser game on mobile"

Mobile

Players join by tapping a link. No app store, no account, no launcher — a phone, a laptop and the one friend on a work iPad all land in the same room. Nothing in the SDK is desktop-only, and the room page renders a QR code precisely so somebody across the table can join without you typing a URL at them.

Two mobile facts worth knowing before you ship: drive your loop from requestAnimationFrame, because timers get starved by touch events; and an iOS tap is replayed ~100 ms later as a synthetic mouse event, so guard your handlers or every button fires twice.

Also: deployed games run in a sandboxed iframe — localStorage throws there, so never key durable data on it.

The timer that survives everything

Once two clients share a clock, "how long is left in the round" becomes the bug that eats an evening. Don't sync a countdown. Have the host write a deadline in server time, once, and let every client subtract: room.now() is a Date.now()-comparable clock agreed across devices, so there's no per-frame write, no drift between phones, and the round keeps ticking correctly straight through a host migration.

The whole API →

Questions from people whose game already works

Can I add multiplayer to a game I already built?

Yes — that is the case this page is written for. Your rendering, your physics, your input handling and your art all stay exactly as they are. What you add is a room object: one import, one await, then two lines inside the loop you already have. Nothing about your game has to be restructured to make room for a netcode layer, because the netcode layer is not in your codebase.

Can't I just ask Claude or Cursor to add multiplayer for me?

You can, and it will write you something plausible: a WebSocket server, a rooms map, a heartbeat, a reconnect loop. The code is often fine. The problem is where it runs. An agent working from your laptop can write a server but cannot be one — no always-on machine, no domain, no certificate, no way to keep a process alive after you shut the lid. What you get is multiplayer that works between two tabs on one computer and nowhere else. Connect antics and the missing half already exists: the agent writes the game-side integration, and we run the part that has to stay awake.

Do I need to run a WebSocket server?

No. That is the whole point of the integration. There is no socket server to write, no room registry to keep in memory, no heartbeat loop, no reconnect backoff, no TLS certificate and no box to keep awake at 2am. antics runs the relay: rooms, presence, ordered events, both state surfaces, host election and leaderboards. Your game opens one connection through the SDK and never sees a socket.

Does this work with Phaser, Three.js, or a plain canvas game?

Yes, and with plain DOM too. The SDK is renderer-agnostic: it is a small ES module that gives you a room object and never touches your canvas, your scene graph or your DOM. If your game runs in a browser and has a game loop, the integration is the same four steps regardless of what draws the pixels.

How do I add multiplayer to an HTML5 game hosted somewhere else?

Two options. Deploy the game to antics and joinRoom({}) needs no configuration at all, because the SDK is served same-origin and the room code arrives in the page URL. Or keep your own hosting: install antics-sdk from npm, create a project to get a publishable pk_ key, add your origin to the project's allowlist, and call joinRoom({ key: "pk_...", game: "<project id>" }). Everything else on this page is identical either way.

How much of my game loop has to change?

Less than people expect. One line where you already move the local player (room.me.setState), one loop over room.players where you already draw, and — if there is a shared world such as NPCs, a round timer or a scoreboard — that simulation moves inside room.onHostTick so exactly one client advances it. Single-player games with no shared world skip the third change entirely.

What happens when the host closes their tab?

The relay re-elects the oldest remaining connection and fires room.onHostChange; if you put your simulation in room.onHostTick it simply starts running in the new host's tab with no special-casing. Authority can also move without anyone leaving — a host that backgrounds its tab hands off so the shared sim doesn't freeze — so treat room.isHost as a value that can flip under you at any moment, and never cache it.

Is the state sync cheat-proof?

The default tier is host-authoritative, not server-authoritative: one player's browser runs the shared simulation, and score submission is anti-spoof rather than anti-cheat — a signed session token, a minimum session age, a maximum score and a submission rate cap. That is the right trade for casual games with friends. If you need a leaderboard that resists a determined player, the sim tier runs your logic on the server instead and records what the simulation says, not what the client claims.

How many players fit in a room, and what does it cost?

A keyless room needs no account at all: 8 players, live sync, an in-room leaderboard, and a link that expires after 24 hours. A free account raises rooms to 16 players, keeps links from expiring, persists leaderboards and allows 25 concurrent rooms. There is no card and no trial clock on either. Full comparison →

One prompt, then go bother your friends.

Your game is finished. The only thing between it and four people shouting at each other over it is a machine that stays awake, and that one's on us. If you'd rather start from something that's already multiplayer than retrofit yours tonight, the make-a-game walkthrough goes idea → link in about five minutes.

claude mcp add antics -- npx -y antics-mcp