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 reportsapi_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 thefieldsarray 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:
-
Solana wallet.
POST wallet_auth.phpwith{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. -
Google account.
POST account_auth.phpwith 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
-
An identity you have not proved is refused, not downgraded.
Naming a wallet you cannot sign for returns
401. It does not quietly fall back to anonymous, because a silent downgrade makes an impersonation attempt look like a success. - Anonymous painting still works. Omit the wallet entirely and the pixel is placed with no attribution. That is a different thing from naming one you do not hold.
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
-
Send the ETag back. An
If-None-Matchheader gets you a304and no body when nothing has changed. A polling bot should almost always be getting 304s. -
Use the watermark, not your own clock. The response carries
X-Canvas-Watermark: the timestamp this data is current as of. Pass it back verbatim tocanvas.php?since=to collect everything after it. Substituting your own clock is the single most common way to miss pixels, because your clock is not the database's. -
Ask for one field.
?field=c|t|cid|n|osreturns that plane alone.cis the colour index and the default;cidis the owning Collab;osis how long the pixel has been held. Fetch only what you draw.
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.
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.
| Status | error | Means |
|---|---|---|
| 401 | sign_in_required | The write needs a token and there was not one. |
| 401 | wallet_requires_signature | You named a wallet that has signed in before, without its token. |
| 403 | wallet_identity_required | Settles in SOL. A Google account cannot do it. |
| 403 | account_identity_required | The mirror image: pledging is for Google accounts, not wallets. |
| 403 | not_campaign_owner | You do not have the role that change requires. |
| 429 | pixel_on_cooldown | That pixel is still locked. The response says for how long. |
| 429 | posting_too_fast | Chat has a short floor between messages. |
| 410 | governance_shelved | Voting 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:
- One placement per pixel per cooldown. The only hard throttle on painting.
- A short floor between chat messages.
- Bulk data export is capped and date-filterable, because an unbounded dump of a million-row table is a denial of service against a small box.
What a considerate bot does:
- Send If-None-Match. A 304 costs the server almost nothing.
- Poll deltas, not the whole canvas. Full reads are for starting up.
- Back off on 429 and 5xx instead of retrying immediately.
- Identify yourself in a User-Agent. If something misbehaves, a name is the difference between a conversation and a block.
Example client
A working Python client, about a hundred lines, covering the part prose is worst at describing — the sign-in handshake:
# 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:
It returns the canvas size, the palette, the live economic constants and every endpoint with a one-line description of what it takes.
Chat and tasks
chat.phpserves a global room, a room per Collab, and direct messages. Poll withsince_idrather than refetching. The global room is swept after 24 hours by design; Collab rooms are kept.tasks.phpis 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.