API reference
The 0xCPU API
Everything the site does, you can do with curl. JSON in, JSON out — except /api/work, which streams binary frames.
Overview
Base URL https://0xcpu.fun. Reading is open. Spending — custom renders, grinds, reservations — needs a session: a signed, HttpOnly cookie you get by signing a Sign-In with Solana message. Requests that change state must come from the same origin or carry no Origin header (e.g. curl).
Money is SOL. Credit is kept in lamports, quotes convert at the Pyth SOL/USD price at quote time, and every job bills the core time its workers measured — never more than the quote, with the remainder refunded when the job completes.
Sign-In with Solana
{ nonce } and sets a signed nonce cookie (10 min).{ message, signature } — the message below for this domain with that nonce, signed with the wallet’s signMessage (ed25519 over the UTF-8 text, signature in base58). Sets a 7-day session cookie.{ address } or { address: null }.0xcpu.fun wants you to sign in with your Solana account:
YourWalletAddress
Sign in to 0xCPU. This signature is free, sends nothing and approves nothing.
URI: https://0xcpu.fun
Version: 1
Chain ID: mainnet
Nonce: <nonce>
Issued At: 2026-09-12T12:00:00.000Z
Expiration Time: 2026-09-12T12:10:00.000ZPulse & health
curl https://0xcpu.fun/api/pulse | jq '{eth, block, stats: {renders: .stats.renders, coreMs: .stats.coreMs}}'Jobs
A job is a signed promise of work: the server validates the request, reserves the quote, and returns a token that authorizes exactly that job’s units. Workers record what they did; completion bills from those records only.
{ "kind": "render" } — the public hero render (16 tiles, 768², no sign-in).{ "kind": "render", "params": { "view": View, "location"? } } — a custom render (session).{ "kind": "grind", "params": { "prefix", "owner"? } } — vanity search for a Solana address derived with createWithSeed(yourWallet, seed, owner); owner defaults to the System Program (session).{
"cx": -0.7453, "cy": 0.1127, // centre in the complex plane
"scale": 3.4e-6, // complex units per pixel
"cos": 1, "sin": 0, // rotation (sent as numbers so both sides agree bit-for-bit)
"maxIter": 800, "width": 1920, "height": 1080,
"ss": 1 // 1, or 2 for 2×2 supersampling
}curl -X POST https://0xcpu.fun/api/jobs -H 'content-type: application/json' -d '{"kind":"render"}'
# → { "job": { "id": "…", "units": 16, "params": { "view": {…}, "tile": 192 }, "tiles": [...] }, "token": "…" }Streaming work
{ token, i } — run unit i of the job. Renders stream four progressively sharper passes of tile i; grinds stream progress while searching seeds for slot i % 16, round ⌊i / 16⌋. Every call ends with a trailer carrying the worker’s CPU, region, timing and a keccak256 receipt.curl -X POST https://0xcpu.fun/api/work -H 'content-type: application/json' \
-d '{"token":"<token>","i":0}' --output tile-0.binFrame format
Each frame is u32 LE length (of type + body), u8 type, then the body.
| Type | Body |
|---|---|
| 1 PASS | u8 pass · u8 stride · u16 gw · u16 gh then deflate-raw of Δmu u16[gw·gh] ‖ inside u8[gw·gh]. mu is the mean smooth iteration ×16 of the escaped samples; Δ is a row-wise delta (mod 2¹⁶); inside counts samples that never escaped. |
| 2 TRAILER | { n, ms, hash, iterations | attempts, receipt, worker: { cpu, cores, memMb, region, node, container, cold } } |
| 3 PROGRESS | { n, attempts, ms, rate, best: { chars, address } } (grinds) |
| 4 ERROR | { n, error } — e.g. tile_timeout; the unit is not billed. |
hash is keccak256 of the final pass’s mu bytes (LE) followed by the inside bytes.
Verifying a tile
Integer iteration counts use only +, −, × and comparisons on IEEE-754 doubles, so every JavaScript engine reproduces them exactly. Re-render the tile with the same kernel and compare: inside must match exactly and mu within 1 (the smooth term uses log2, whose last bit may differ; any integer mismatch moves mu by at least 4).
import { renderTile, compareTiles } from "./lib/compute/mandelbrot";
const local = renderTile(view, tile); // same View + Tile the server used
const { match, total } = compareTiles(received, local);
console.log(match === total ? "identical" : `${total - match} pixels differ`);Credit & top-ups
{ signature }. Credits the sender of a SOL transfer to the top-up wallet once the transaction is finalized; returns 202 pending until then. Idempotent — resubmitting never double-credits.{ transaction } (base64, already signed). Broadcasts it for wallets that can’t send on their own.Reservations
{ tier, region, term: "hourly"|"monthly"|"yearly", qty, note? } — a request, nothing is charged.Limits
| Limit | Value |
|---|---|
| Hero renders | 8 per network per 10 min · 150 per day for everyone |
| Free core-time | 10 core-minutes per wallet per UTC day |
| Custom renders | ≤ 3840×2160, ≤ 3000 iterations, ≤ 96 tiles, ss 1 or 2 |
| Vanity search | ≤ 268 M expected tries (≤ 16.7 M on the free allowance), base58, case-sensitive |
| Per unit | 8 s of compute; job tokens live 5 min (renders) / 10 min (grinds) |
| Top-ups | ≥ 0.01 SOL, SOL only |
Errors
Errors are JSON { error, detail? } with a meaningful status: 400 bad input, 401 sign_in_required / bad_token, 402 insufficient_credits / free_allowance_exhausted, 409 job_closed / fresh, 429 rate_limited / budget_exhausted, 503 market_unavailable / busy.