ArtWars shield logo ArtWars

The ArtWars API

Everything on the canvas is reachable over plain HTTP and JSON. Writing a bot is a supported way to play, not something the game defends against.

Start here

The base URL is https://artwars.io/api/. Every endpoint is a single PHP file under it, every response is JSON unless stated otherwise, and GET api/ returns a machine-readable catalogue of the whole surface so a tool can discover it without reading this page.

Read the entire canvas, right now, with no account:

curl -s https://artwars.io/api/canvas_blob.php --output canvas.bin
# 1,048,576 bytes. One byte per pixel. offset = y * 1024 + x
# The value is an index into the 16-colour palette.

In a browser that is two lines:

const res = await fetch('https://artwars.io/api/canvas_blob.php');
const pixels = new Uint8Array(await res.arrayBuffer());

There is no API key and no registration. Reading is open to anyone.

Ground rules

Bots are welcome
Scripted play through this API is an intended feature. The per-pixel cooldown is the only throttle on painting, and it is deliberately not per-account, so one identity may work on many different pixels at once.
Reads are open, writes are not
Anything that only looks at the world needs no identity at all. Anything that changes it needs a signed-in one.
Versioned, as of now
Every response carries X-ArtWars-API-Version, and the catalogue reports api_version. It bumps only on a breaking change — a removed field, a renamed field, a changed shape. Adding a field is not breaking, so read the fields array a response gives you rather than assuming a shape. Currently version 1.
Usable from a browser, on any origin
Access-Control-Allow-Origin: * on every endpoint, reads and writes. That is safe here for a specific reason: auth is a bearer token, never a cookie. A browser attaches cookies to cross-origin requests by itself, which is what makes CSRF possible; it will never attach a token it has to be told about. So this opens nothing that curl could not already reach.
No money moves
Funding amounts are in units of one tenth of a US cent, and they are test stand-ins. There is no payment path anywhere in this application yet, so nothing you call can spend real SOL.

Identity

Authentication is a bearer token. Send it as a header, which is the form to prefer in anything new:

Authorization: Bearer <token>

A token field in a JSON body and a ?token= query parameter both work too. The body is easiest for a bot posting JSON and the query string is there for quick curl testing.

Getting a token

Two identity systems, independent of each other:

  1. Solana wallet. POST wallet_auth.php with {action: "challenge", wallet} returns a nonce and the exact message text. Sign that message with the wallet's key, then {action: "verify", wallet, signature} returns the token. Signing is not a payment and moves nothing.
  2. Google account. POST account_auth.php with a Google ID token. Intended for a browser using Google Identity Services rather than for a headless bot.

The server verifies a signature against its own stored copy of the challenge, never against text you supply, so a signature made for another site cannot be replayed here.

Two things that will bite you

Reading the canvas

canvas_blob.php is the fast path and the one to build on. It returns the whole canvas as raw bytes: one request, about 232KB gzipped, and no parsing. The JSON route is roughly 23MB and sixteen requests.

Reading it efficiently

X-Canvas-Element-Bytes tells you how wide each element is, so you can pick a typed array without hardcoding it.

Placing pixels

curl -s https://artwars.io/api/place.php \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <token>' \
  -d '{"x": 512, "y": 512, "color_index": 5}'

color_index is 0 to 15, into the palette the catalogue returns. A pixel that is still cooling down answers 429 with pixel_on_cooldown and tells you how long is left, so a bot should read that field rather than guessing an interval.

The cooldown is per pixel, not per player. A bot working across a wide area is never waiting.

Collabs

A Collab is a group-funded artwork that paints and defends itself. GET campaigns_list.php returns every Collab that is on the canvas. GET campaign_map.php?campaign_id= is the interesting one for a bot: it returns per-pixel blueprint versus reality — which pixels are done, wrong, contested or still available — which is exactly the input a helper bot needs.

Creating one is free and needs no wallet. Funding one settles in SOL and does.

Blueprint index 0 is blank: not part of the artwork, never painted, never defended, and not counted toward the funding target.

Chat and tasks

chat.php serves a global room, a room per Collab, and direct messages. Poll with since_id rather than refetching. The global room is swept after 24 hours by design; Collab rooms are kept.

tasks.php is claimable work inside a Collab. Completion is computed live from the canvas rather than self-reported, so a task correctly un-completes if a rival takes the ground back.

Errors

Every failure is a JSON object with an error field holding a stable string. Compare on that string, never on the prose, which is free to change. Some errors add a detail you can show a person.

StatuserrorMeans
401sign_in_requiredThe write needs a token and there was not one.
401wallet_requires_signatureYou named a wallet that has signed in before, without its token.
403wallet_identity_requiredSettles in SOL. A Google account cannot do it.
403account_identity_requiredThe mirror image: pledging is for Google accounts, not wallets.
403not_campaign_ownerYou do not have the role that change requires.
429pixel_on_cooldownThat pixel is still locked. The response says for how long.
429posting_too_fastChat has a short floor between messages.
410governance_shelvedVoting is switched off. Reads still work.

Limits and etiquette

600 requests per minute, per IP, across all endpoints. Over that you get 429 rate_limited with a Retry-After header and a retry_after_seconds field.

It is set high on purpose. Bots are a supported way to play, so this is aimed at runaway loops rather than at scripted play — a poller that sends If-None-Match uses a small fraction of it. The number exists because this runs on a small box with a handful of PHP workers, and the limit's real job is stopping one client from occupying all of them.

On top of that:

What a considerate bot does:

Example client

A working Python client, about a hundred lines, covering the part prose is worst at describing — the sign-in handshake:

examples/artwars_bot.py

# read-only, standard library only
python3 artwars_bot.py info
python3 artwars_bot.py show 512 512

# sign in and paint  (pip install pynacl base58)
python3 artwars_bot.py paint 512 512 5 --key ~/my_wallet.json

It prints the message before signing it. Never sign one a tool will not show you. Three details in there are easy to get wrong and are commented in the source: the nonce must go back with the signature, the message must be signed exactly as returned, and errors must be matched on the error string rather than the prose.

Full reference

The complete endpoint list is generated by the server itself, so it cannot drift from what is actually running the way a hand-written page can:

GET https://artwars.io/api/

It returns the canvas size, the palette, the live economic constants and every endpoint with a one-line description of what it takes.