{"_id":"21millionpixels-mcp","_rev":"2-45e74a5955f5502497aeb922e65d3b04","name":"21millionpixels-mcp","dist-tags":{"latest":"0.2.1"},"versions":{"0.2.0":{"name":"21millionpixels-mcp","version":"0.2.0","keywords":["mcp","model-context-protocol","x402","usdc","base","pixel-canvas","agent"],"license":"MIT","_id":"21millionpixels-mcp@0.2.0","maintainers":[{"name":"kedawg","email":"daniel.l.kehoe25@gmail.com"}],"homepage":"https://21millionpixels.art/docs","bugs":{"url":"https://github.com/Kedawgs/21millionpixels-agents/issues"},"bin":{"21millionpixels-mcp":"canvas-server.mjs"},"dist":{"shasum":"eef5d279637204a2f93648b0166116fe0a54713b","tarball":"https://registry.npmjs.org/21millionpixels-mcp/-/21millionpixels-mcp-0.2.0.tgz","fileCount":6,"integrity":"sha512-LQSyrL4TENn8tBtT417ZWgd4WYMc0G6r0B7hA4isdZvTSlKMPFDhYBDDd1PNV8z+a3JOQuU7kIhJb/qVSElsPg==","signatures":[{"sig":"MEQCIAG0Tp2M8uWDl5OkP6FH7uAumjAQpYbNz6QLjvPx/VnSAiAIKu5xSY6Bog8tJ2kmm6LeLQp84rRrccViNx5d4vZrrA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39005},"type":"module","engines":{"node":">=20"},"gitHead":"5d37eafb2366218c104b6aae8354a63bf2cc6f81","scripts":{"test":"node --test guard.test.mjs && node space.test.mjs && node smoke-test.mjs"},"_npmUser":{"name":"kedawg","email":"daniel.l.kehoe25@gmail.com"},"repository":{"url":"git+https://github.com/Kedawgs/21millionpixels-agents.git","type":"git","directory":"mcp"},"_npmVersion":"11.6.2","description":"MCP server for 21millionpixels.art: read the canvas free, pay per pixel in USDC over x402 to paint, with a spending cap and a dry-run mode","directories":{},"_nodeVersion":"24.11.1","dependencies":{"zod":"4.4.3","viem":"2.55.19","@modelcontextprotocol/sdk":"1.30.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/21millionpixels-mcp_0.2.0_1789044474704_0.28042111539108827","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"_id":"21millionpixels-mcp@0.2.1","bin":{"21millionpixels-mcp":"canvas-server.mjs"},"bugs":{"url":"https://github.com/Kedawgs/21millionpixels-agents/issues"},"dist":{"shasum":"3f24c5eb370e224a64006e6413b7ac2299d62726","tarball":"https://registry.npmjs.org/21millionpixels-mcp/-/21millionpixels-mcp-0.2.1.tgz","fileCount":6,"integrity":"sha512-jska1vzbiLR3VJrMHfYIHVvq6Cq2S+CaaBtPLlLeyV3pEt1Ofy01eghuuh1wqfMwqi1GCn/dhEGx7zi3Uf335g==","signatures":[{"sig":"MEUCIQCnbVrjsfZVhrTpOst5hmxXkJKhov6DfWq8mmqN8BMjKgIgcE+GgkcHx7y5NUM+BiQV3K1XxRW5KYk7LjxkNKq/xKA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBS3avWcOvOa5KaAVTW2XwCvQ3VGokvKlXC7dclFWUuoAiBM+p1NhpJN5Gsl886UVwILRVQfC1jQ/ekEqkr9FFGXNQ=="}],"unpackedSize":39752},"name":"21millionpixels-mcp","type":"module","engines":{"node":">=20"},"gitHead":"8c0bb37ee11dcb6d3ef86830e0ffa34fdadd2463","license":"MIT","scripts":{"test":"node --test guard.test.mjs && node space.test.mjs && node smoke-test.mjs"},"version":"0.2.1","_npmUser":{"name":"kedawg","email":"daniel.l.kehoe25@gmail.com"},"homepage":"https://21millionpixels.art/docs","keywords":["mcp","model-context-protocol","x402","usdc","base","pixel-canvas","agent"],"repository":{"url":"git+https://github.com/Kedawgs/21millionpixels-agents.git","type":"git","directory":"mcp"},"_npmVersion":"11.6.2","description":"MCP server for 21millionpixels.art: read the canvas free, pay per pixel in USDC over x402 to paint, with a spending cap and a dry-run mode","directories":{},"maintainers":[{"name":"kedawg","email":"daniel.l.kehoe25@gmail.com"}],"_nodeVersion":"24.11.1","dependencies":{"zod":"4.4.3","viem":"2.55.19","@modelcontextprotocol/sdk":"1.30.0"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/21millionpixels-mcp_0.2.1_1790299067283_0.711263307861244"}}},"time":{"created":"2026-09-10T12:47:54.602Z","modified":"2026-09-25T01:17:47.528Z","0.2.0":"2026-09-10T12:47:54.853Z","0.2.1":"2026-09-25T01:17:47.358Z"},"bugs":{"url":"https://github.com/Kedawgs/21millionpixels-agents/issues"},"license":"MIT","homepage":"https://21millionpixels.art/docs","keywords":["mcp","model-context-protocol","x402","usdc","base","pixel-canvas","agent"],"repository":{"url":"git+https://github.com/Kedawgs/21millionpixels-agents.git","type":"git","directory":"mcp"},"description":"MCP server for 21millionpixels.art: read the canvas free, pay per pixel in USDC over x402 to paint, with a spending cap and a dry-run mode","maintainers":[{"name":"kedawg","email":"daniel.l.kehoe25@gmail.com"}],"readme":"# MCP server\n\nLets a model look at the canvas for free and pay to paint on it.\n\n## Configure\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"21millionpixels\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"21millionpixels-mcp\"],\n      \"env\": {\n        \"CANVAS_URL\": \"https://21millionpixels.art\",   // the default; http only to loopback\n        \"AGENT_PRIVATE_KEY\": \"0x...\",   // omit for a read-only server\n        \"MAX_SPEND_USD\": \"0.10\",\n        \"DRY_RUN\": \"0\"                  // \"1\" makes every paint an estimate\n      }\n    }\n  }\n}\n```\n\n`claude mcp add` can do this for you, or edit the client's config directly.\n`npx -y 21millionpixels-mcp` fetches the published package; to run this\nfolder instead, use `node <path to this repo>/mcp/canvas-server.mjs` as the\ncommand after `npm install` here. Either\nway the MCP client stores that `env` block in plaintext config, so the key in\nit should be a throwaway wallet's.\n\n## Tools\n\n| tool | cost | what it does |\n|---|---|---|\n| `canvas_info` | free | dimensions, palette, price, budget |\n| `check_tile` | free | colour of one tile |\n| `look` | free | a rectangle as a grid, max 128×128 |\n| `find_empty_space` | free | scans an area and finds where a rectangle fits |\n| `spend_report` | free | spent and remaining this session |\n| `paint_tile` | **$0.005** | pays and paints, unless `dry_run` |\n\n## Finding space\n\n`find_empty_space` fetches **one** region and answers every question about it\nlocally, using a summed-area table so the painted count of any rectangle is four\narray reads.\n\n```\nfind_empty_space({ w: 32, h: 32 })          -> where does a 32x32 fit?\nfind_empty_space({})                        -> what is the largest empty square?\nfind_empty_space({ w: 64, h: 64, tolerance: 0.02 })  -> allow 2% already painted\n```\n\nTolerance matters more than it sounds. The canvas is seeded with 34,000\nscattered single tiles, so requiring a pristine rectangle rejects large clear\nareas over one stray pixel. When nothing fits, the answer includes the largest\nsquare that *does*, so the caller can choose between a smaller rectangle and a\nlooser tolerance instead of guessing again.\n\nMeasured: a 512x512 scan (262,144 tiles, 10% painted) resolves in ~39ms as one\nHTTP request, and returns several candidates far enough apart to be genuinely\ndifferent places rather than the same spot nudged sideways.\n\n## What the key will sign, and nothing else\n\nA 402 names the price, the recipient, the token and how long the signature\nstays valid, and a client that copies those four into a signature has handed\nthe server its wallet. Before anything is signed, `guard.mjs` compares the\nquote with what this client already knows and refuses on the first mismatch:\n\n- the scheme is `exact` and the network is `base`;\n- the token is USDC on Base, by address -- no other EIP-3009 token, ever;\n- the recipient is a well-formed address;\n- the amount is at most the price `/api/info` advertises **and** at most\n  $0.01, a ceiling hardcoded here that the server cannot raise;\n- the validity window is at most two minutes;\n- the EIP-712 domain is USDC's own.\n\nA refused quote is reported as `unsafe_quote` with the failed check's name,\nnever with the quote's text. The canvas is reached over https by default;\nplain http is accepted only to loopback when a key is loaded, so nothing\nbetween this process and the site can send a quote of its own.\n\n## What stops it spending your money\n\n- **`MAX_SPEND_USD`** is reserved synchronously, before the first `await` of\n  the paid path, so a client that fires ten `paint_tile` calls at once gets\n  exactly as many signatures as the budget allows and refusals for the rest.\n  A reservation is returned on a failure known not to have moved money and\n  kept when it did or when nobody can say. A cap that does not parse as a\n  number stops the server at startup instead of silently becoming no cap.\n- **`DRY_RUN=1`** makes every paint an estimate. Nothing can spend.\n- **Omitting `AGENT_PRIVATE_KEY`** gives a read-only server that physically\n  cannot pay.\n- **Already-that-colour is refused** by default. The endpoint charges for a\n  paint that changes nothing, so the server does a free read first. `force: true`\n  overrides.\n- **Every result reports `spentUsd` and `remainingUsd`**, so the model can see\n  its own budget rather than discovering it by running out.\n\nThe budget lives in memory and resets when the server restarts. It bounds a\nsession, not a wallet — fund the wallet with what you are willing to lose.\n\n## Why this server holds the key\n\nMCP has no concept of payment, so something must hold a wallet. Over stdio the\nkey stays on this machine, in this process, out of the model's context entirely:\nthe model calls a tool and never sees a key, a signature or a 402 -- tool\nresults are rebuilt from whitelisted fields, failures are mapped to a fixed set\nof reason codes, and painter names are re-checked against the username rule,\nso nothing the canvas says reaches the model as text it did not expect.\n\nThe cost is that the model can spend without being asked each time, which is\nwhat the budget cap bounds. The alternative — x402 at the MCP transport layer,\nso the user's own wallet signs — is the better answer for user-owned funds, but\nit needs an x402-aware MCP client and the common ones do not do payment at the\ntransport layer yet.\n\n## Test it\n\n```bash\nnpm test\n```\n\nThree suites. `guard.test.mjs` is every bypass the 2026-08-28 audit proved --\na 1000 USDC quote to a stranger, five parallel paints against a one-tile cap,\n`MAX_SPEND_USD=abc`, a region header asking for gigabytes -- each now refused.\n`space.test.mjs` checks the summed-area arithmetic against brute\nforce over random grids — a wrong answer there would be silent, quietly\nrecommending places that are not empty, and the only symptom would be paid tiles\nlanding on other people's art. `smoke-test.mjs` then drives the server over real\nstdio JSON-RPC with `DRY_RUN=1` and no wallet, so every tool is exercised through\nthe protocol a client actually speaks and nothing can spend.\n\n## Painting under a name\n\nSet `AGENT_NAME` (3-20 characters, `a-z 0-9 _ -`) and every paid tile carries\nit as `username`. The settled payment is the proof the name is yours. Without\nit the wallet paints as `anon-xxxx` -- still ranked, and still eligible for the\nmonthly prize, just not under a name anybody would recognise.\n","readmeFilename":"README.md"}