API reference
Base URL https://api.arcloak.net. Requests and responses are JSON, except the chat stream. Amounts are strings of 6-decimal USDC units: 1000000 is 1 USDC. Addresses are 0x plus 40 hex characters, in any case. CORS allows any origin and exposes PAYMENT-REQUIRED, PAYMENT-RESPONSE and X-Payment-Transaction.
Errors share one shape, with a stable code and a readable message:
{"error":{"code":"not_found","message":"unknown node"}}
The proxy in front of the API can also answer 429, 502, 503 or 504 with an HTML page; treat those as retryable. The chat route accepts 5 requests per second per IP with a burst of 20, and the withdraw route 2 per second with a burst of 5. Above that the proxy answers 503.
GET /health
Liveness and live capacity. Response 200:
{"ok":true,"chainId":5042,"nodes":2,"models":{"llama3.2:3b":2,"qwen2.5:7b":0,"llama3.1:8b":1,"qwen2.5:32b":0}}
nodes counts the nodes connected now; models counts them per catalog model.
GET /v1/models
The catalog in the OpenAI list shape, with prices and live node counts. Response 200 is {"object":"list","data":[…]}, and each entry has:
id,object(always"model"),owned_by(always"arcloak") andtier(small,midorlarge).price_usdc_per_million, withinputandoutputas decimal USDC strings: the numbers on the Pricing page.nodes_online: the nodes serving this model now.
The response may be cached for 15 seconds.
POST /v1/chat/completions
Paid through x402 upto; see How x402 upto works. The request body is the OpenAI chat shape, the subset below:
model(required): a catalog model id.messages(required): 1 to 64 objects of the form{"role": "system" | "user" | "assistant", "content": "…"}, at most 200000 characters in total.max_tokens: a positive integer, default 512, lowered when needed so the ceiling fits under the 2 USDC per-request cap.temperature: from 0 to 2.stream:truefor server-sent events; anything else returns one JSON response.
Successful responses:
200with"stream": true:text/event-stream. Eachdata:line carries an OpenAIchat.completion.chunk. The last chunk has"finish_reason": "stop"andusage. Then comes{"object":"x402.payment","transaction":"0x…","amount":"3","network":"eip155:5042"}, thendata: [DONE]. A failure after the stream started arrives asdata: {"error":{"code":"…","message":"…"}}before[DONE].200with"stream": false: achat.completionobject withusage. TheX-Payment-TransactionandPAYMENT-RESPONSEheaders carry the settlement.
Errors:
400invalid_request: a malformed body or field, or a prompt that leaves no room for output under the cap. The message says which.400model_not_found:modelis not in the catalog.402payment_required: no payment was attached, or the attached one failed verification. The terms, and the reason on a failed verification, are in thePAYMENT-REQUIREDheader.409payment_in_use: the same payment authorization is already serving another request. Wait for it to finish and try again.402settlement_failed, or the same code as a stream error event: the facilitator refused the settlement, so nothing was charged.502settlement_error, or the same code as a stream error event: the settlement outcome is unknown. Check the payer's history before retrying.503node_unavailable: no node serves the model, or the node stopped before its first token. The authorization is cancelled.503unavailable: the job could not be recorded. Nothing was charged.chunk_limit, a stream error event after the payment event: the node sent more thanmax_tokensallows. The answer was cut and only the delivered part was billed.
GET /v1/nodes/{address}
A node's public record. Response 200:
{"address":"0x7d826b2c78744c2c6c7a9afd4feeaf97e4b0e487","online":true,"models":["llama3.2:3b"],"balance":"4","jobs_total":2,"last_nonce":"0","min_withdraw":"1000000"}
Errors: 400 invalid_request for a malformed address, 404 not_found for a node that never connected.
GET /v1/nodes/{address}/history
The node's ledger rows, newest first. Query limit: 1 to 200, default 50. Response 200:
{"address":"0x7d82…e487","entries":[{"id":3,"reason":"withdraw","delta":"-1000000","withdraw_id":1,"tx":"0x…","created_at":"2026-09-27T10:00:00.000Z"},{"id":2,"reason":"job","delta":"2","job_id":"…","created_at":"2026-09-27T09:00:00.000Z"}]}
reason is job (a credit for a settled answer), withdraw (a debit) or refund (a failed withdrawal returned). Errors: 400 invalid_request for a bad address or limit, 404 not_found.
POST /v1/nodes/{address}/withdraw
Pays amount from the node's balance to the node address. Body:
{"amount":"1500000","nonce":"5","signature":"0x…"}
amount and nonce are integer strings. signature is an EIP-191 personal_sign, by the node key, of arcloak-withdraw:5042:<address lowercase>:<amount>:<nonce>, where the nonce is above the node's last_nonce. Response 200:
{"id":9,"tx":"0x…","amount":"1500000"}
Errors:
400invalid_request: a malformed address or body.400below_minimum: less than 1 USDC (1000000units).401bad_signature: the signature does not recover to the node address.404not_found: the node never connected.409stale_nonce: the nonce is not abovelast_nonce. Reload the node and use the next one.409insufficient_balance: the amount is above the balance.503wallet_below_floor,daily_caporpayout_busy: the payout wallet cannot pay right now. The balance is unchanged.502transfer_unconfirmed: the transfer was sent but not confirmed. The withdrawal stays pending until it is reconciled; do not retry.502transfer_failed: nothing was sent and the balance is unchanged.
GET /node
The WebSocket that node software connects to, at wss://api.arcloak.net/node. It is not a buyer API: the node signs a challenge with its key, advertises its models, then receives jobs and streams chunks back. See the Node quickstart.
Every route
404not_found: an unknown path.500internal: an unexpected server error.