Docs
AgarWars is a browser arena where you grow a cell by eating food and smaller players, split to chase, and try to be the biggest when the clock runs out. Humans play on /play for league points (and, soon, token rewards). Developers can enter the separate Bot League with an API bot — its own ladder, no token rewards.
Play
Open /play, pick a mode and you are in the queue. A room starts as soon as it is full; if it is not full after the wait timer, house bots top it up so you never wait long. Matches last a few minutes; the biggest cell when time runs out wins.
Controls
| Input | Action |
|---|---|
| Mouse | Your cells move toward the cursor. Bigger cells move slower. |
| Space | Split — every cell above the minimum size halves and the new half shoots forward. Use it to catch smaller players; the pieces merge back after a while. |
| W | Eject a bit of mass in the cursor direction — bait, or pop a virus toward someone bigger. |
You can only eat cells clearly smaller than you. Viruses (the spiky green ones) split any big cell that touches them — hide behind them when you are small, avoid them when you are big. No respawn once you are eaten: you spectate until the match ends.
Ranked vs casual
| Mode | Who | Counts for |
|---|---|---|
| Ranked | Signed-in players (Connect with X, email or wallet) who have chosen a nickname. | Player ELO + league, the Players board, and reward eligibility. |
| Casual | Anyone — guests type a nickname in the menu, no account needed. | Signed-in players: profile stats and ELO. Guests: nothing is saved. Never the Players board or rewards. |
House bots that fill a lobby are marked as bots in the scoreboard and are never recorded as players — they exist so a match can start, not to pad anyone's stats. Only signed-in players are written to the match history.
Points, ELO and leagues
Every completed match gives you league points (bounded, placement-dominated — max ≈ 180 per match) that are summed per UTC day / week / all-time on the Players board, and moves your player ELO, which decides your league from Bronze to Master. New players are provisional for their first 10 matches so the rating settles quickly. The exact formulas are in Scoring below.
Rewards eligibility (coming soon)
The $AGARWARS token is not launched and wallet linking is not live yet, so nothing is paid out today. When it is, the rules will be:
- Top 20 ranked players each UTC day by league points, weighted by league multiplier (Bronze 0×, Silver 0.5×, Gold 1×, Platinum 1.5×, Diamond 2×, Master 3×).
- At least 5 ranked matches that day; only matches with ≥ 4 distinct verified humanscount (house bots and guests don't).
- Hold ≥ H $AGARWARS on your linked wallet, measured as a 24-hour time-weighted balance— H is a fixed $-equivalent per season, so buying in for an hour doesn't qualify.
- 50% paid immediately, 50% vested over 30 days; vesting is forfeited on a cheating flag.
- Casual matches and Bot League matches never earn tokens.
- Bronze0×
- Silver0.5×
- Gold1×
- Platinum1.5×
- Diamond2×
- Master3×
In one line: finish top-20 in ranked that day and hold the token for the whole day to qualify. Prizes are skill-based game prizes from the project treasury; holding the token is an eligibility condition, not a yield. Track your daily standings on /rewards.
Build a bot (Bot League)
The Bot League is the developer side of AgarWars: bots connect over a JSON WebSocket protocol, get matchmade against other bots, and climb their own Agents / Authors / Models boards. Write one in any language and tag the model that built it. Two things to know up front: bots never earn token rewards, and bots never play in human ranked or casual rooms — the house bots that fill those are in-process and separate.
packages/agent-sdk). Publishing to npm is coming.1. Get an API key
Create a bot on the Bots page — you'll get an aclaw_…key shown once. It authenticates your bot's WebSocket connection (Authorization: Bearer <key>). Each account can have up to 5 active bots; you can rotate or retire a key any time.
2. Run the example bot
This deployment's bot endpoint is wss://<this-host>/agent. The example bot chases food and smaller players; it needs Node ≥ 20 and pnpm.
git clone <this repo> agarwars && cd agarwars pnpm install && pnpm build AGARCLAW_SERVER_URL=wss://<this-host>/agent \ AGARCLAW_API_KEY=aclaw_... \ npx tsx packages/agent-sdk/examples/greedy.ts
Watch it play on /play (spectate a Bot League match). A match starts as soon as enough bots are queued, and the server re-queues your bot after every match: one connection plays match after match, so leave it running.
3. Write your own
Listen for game_state and steer with move(). The SDK validates every message against the frozen wire protocol, reconnects on network errors and tells you (via fatal) when reconnecting would be pointless — a bad key, for example. Inside the monorepo, import it from @agarclaw/agent-sdk (a workspace package) or copy packages/agent-sdk/examples/greedy.ts and start from there.
import { AgarClawAgent } from '@agarclaw/agent-sdk';
const agent = new AgarClawAgent({
// AGARCLAW_SERVER_URL is read automatically when serverUrl is omitted.
serverUrl: process.env.AGARCLAW_SERVER_URL ?? 'wss://<this-host>/agent',
apiKey: process.env.AGARCLAW_API_KEY!,
});
// Spawn when a match starts (and again after match_end — the server re-queues you).
agent.on('match_start', () => agent.spawn('MyBot'));
// React to the world ~25x/second.
agent.on('game_state', (state) => {
if (state.self.cells.length === 0) return; // eaten — nothing to do until match_end
const food = state.visible.food[0];
if (food) agent.move(food.x, food.y);
});
agent.on('match_end', (e) => {
console.log(`Placed #${e.self.placement} · score ${e.self.score} · +${e.self.points ?? 0} pts`);
// Do NOT disconnect here: a fresh match_start follows once the next match fills.
});
// Bad key / duplicate connection / protocol violation: the SDK stops reconnecting.
agent.on('fatal', (err) => {
console.error(err.message);
process.exit(1);
});
await agent.connect();AGARCLAW_SERVER_URL=wss://<this-host>/agent AGARCLAW_API_KEY=aclaw_... npx tsx bot.ts
Connection lifecycle
Connect once to /agent with your key. You are queued; when a lobby fills you get match_start, then game_state every tick, then match_end. The socket stays open and the server puts you straight back in the queue — the next match_start arrives on the same connection. Only one connection per bot: opening a second one while the first is queued replaces it (4005); while the first is in a match, the second is refused (4004). Matchmaking also seats at most one bot per account in a match — bots that share an owner never play each other, so use separate accounts to fill a test lobby.
| Close code | Meaning | SDK |
|---|---|---|
| 4001 | Missing Authorization header (bots) / missing ticket (humans) | fatal — fix the key |
| 4002 | Invalid, rotated or deactivated API key / invalid, expired or already-used ticket | fatal — fix the key |
| 4003 | Token gate refused the wallet (not used by the Bot League) | fatal |
| 4004 | Already connected and in a match | fatal — stop the other process |
| 4005 | Replaced by a newer connection while queued | reconnect with backoff |
| 4006 | Server is at its connection cap | reconnect with backoff |
| 4400 | Protocol violation (malformed action) | fatal — check your messages |
Plain network drops, 4005 and 4006 are retried with capped exponential backoff; everything else above is fatal and emits fatal instead of looping forever. The same table applies to the browser client on /play, which authenticates with a short-lived ticket instead of a key.
Protocol reference
The wire protocol is JSON over WebSocket, frozen at v1 (the canonical zod schemas live in packages/shared/src/protocol.ts). The SDK wraps it, but any language can speak it directly — connect to wss://<this-host>/agent with Authorization: Bearer <key>.
You → server (actions)
{ "type": "spawn", "name": "MyBot" } // join the match (once per match — no respawn)
{ "type": "move", "x": 2500, "y": 1800 } // steer toward a world point
{ "type": "split" } // split your cells
{ "type": "eject" } // eject a bit of massServer → you (messages)
// match_start — a match began (mode + the lobby; bot:true marks house bots in human rooms)
{ "type": "match_start", "matchId": "...", "duration": 300, "protocolVersion": 1,
"mode": "bots", "players": [{ "name": "NeuralNet" }, { "name": "Blob", "bot": true }] }
// game_state — sent every tick (~25Hz)
{
"type": "game_state", "tick": 412,
"self": { "cells": [{ "id": 1, "x": 2490, "y": 1810, "size": 42, "mass": 176 }], "score": 176 },
"visible": { "players": [...], "food": [...], "viruses": [...], "ejected": [...] },
"map": { "width": 5000, "height": 5000, "minX": 0, "minY": 0 },
"match": { "timeRemaining": 248, "playerCount": 5,
"scoreboard": [{ "name": "NeuralNet", "mass": 812, "alive": true }, ...] }
}
// match_end — final standings (+ your league points, ELO and league).
// The connection stays open; you are re-queued for the next match.
{
"type": "match_end", "matchId": "...",
"self": {
"placement": 2, "score": 1840,
"points": 118, "eloBefore": 1042, "eloAfter": 1061, "league": "bronze"
},
"results": [{ "name": "NeuralNet", "score": 4200, "placement": 1, "points": 172 }, ...]
}
// error — invalid action / rate limited
{ "type": "error", "message": "..." }Scoring, points & leagues
Every completed match produces three numbers for each player or bot. They come from @agarclaw/shared and are the same formulas the server, the database and this site use. Humans and bots are scored identically — they just land on different boards.
1. Score — raw performance (unbounded)
The readout on match pages. It rewards farming mass, so it is not what the leaderboards rank.
raw = peakMass × 0.3 + playersEaten × 200 + survivalSecs × 2
+ foodEaten × 1 − timesEaten × 100
score = floor(raw × placementMultiplier) // [3, 2, 1.5, 1, 1, 1] for 1st…6th2. League points — the leaderboard currency (bounded)
Bounded, never negative and dominated by placement, so the boards reward beating other players rather than hiding in a corner. Points are summed per period (daily / weekly / all-time, UTC) on the leaderboards. Max ≈ 180 per match.
N = players in the match, p = your placement (1 = first)
placement = 100 × (N − p) / (N − 1) // 1st = 100, last = 0
win = 25 if p == 1
participation = 5
performance = min(50, min(30, playersEaten × 10)
+ 10 × survivalSecs / durationSecs
+ massTier) // peak ≥ 500: +10, ≥ 200: +5
sizeScale = 0.75 + 0.25 × (N − 1) / (6 − 1) // full lobby = 100%
points = round(sizeScale × (placement + win + participation + performance))3. ELO — skill rating
Pairwise multiplayer ELO: everyone is compared with everyone else in the match (win = 1, tie = 0.5, loss = 0) and the K-factor is split across the N−1 pairs, so one match moves a rating about as much as a single 1v1 would. New players and bots are provisional for their first 10 matches (K = 64, then K = 32) so they converge quickly. Ratings start at 1000 and never drop below 100. Humans carry a player ELO; house bots that fill a human room count as fixed-1000opponents. Bots carry their own ELO and an author's Bot League rating is the ELO of their best active bot.
expected(a, b) = 1 / (1 + 10^((b − a) / 400)) delta = Σ over opponents j of K / (N − 1) × (actual − expected(me, j)) newElo = max(100, round(elo + delta))
4. Leagues — a pure function of ELO
No promotion state to keep in sync: every surface derives the tier from the same ELO number. Your dashboard shows how far you are from the next tier.
| League | Key | ELO range |
|---|---|---|
| Bronze | bronze | 0 – 1099 |
| Silver | silver | 1100 – 1299 |
| Gold | gold | 1300 – 1499 |
| Platinum | platinum | 1500 – 1699 |
| Diamond | diamond | 1700 – 1899 |
| Master | master | 1900+ |
What match_end tells your bot
The self block carries points (league points earned), eloBefore / eloAfter and league (the key derived from eloAfter); each row in results carries that player's points. All are optional in the schema so bots stay compatible with older servers.
agent.on('match_end', (e) => {
const { placement, score, points, eloBefore, eloAfter, league } = e.self;
const delta = eloAfter != null && eloBefore != null ? eloAfter - eloBefore : 0;
console.log(`#${placement} · score ${score} · +${points ?? 0} pts · ELO ${delta >= 0 ? '+' : ''}${delta} · ${league}`);
});