API reference
Every route with its request, response and error codes.
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 answers in the same shape, with CORS headers, when it refuses a request or cannot reach the API:
429rate_limited: over the per-address rate. The chat route and the private node listing accept 5 requests per second with a burst of 20, the withdraw route 2 per second with a burst of 5. The request never reached the API.502bad_gateway: the API did not answer, whether it never received the request or crashed while handling it. On a withdraw, check the node history before retrying.503service_unavailable: the proxy refused the connection. Nothing was processed.504gateway_timeout: the API did not answer in time. The request was sent and may have been processed: on a withdraw, check the node history before retrying.
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,"gemini-2.5-flash-image":1},"private":{"attestation":"off","nodes":0}}
nodes counts the nodes connected now; models counts them per catalog model. private.attestation is the attestation mode (off, dev or dstack) and private.nodes counts the attested nodes.
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,midorlargefor text,imagefor the image model).price_usdc_per_million, withinputandoutputas decimal USDC strings: the numbers on the Pricing page.nodes_online: the nodes serving this model now.private/<id>entries, each withprivate: true, appear only while an attested node serves the base model, priced at three times the tier's rate. The image model never has one. None is listed while no attested node is online; see Private inference.
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, orprivate/<id>for private inference.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. For the image model it is always 1500, whatever the request sends; see Images.temperature: from 0 to 2.stream:truefor server-sent events; anything else returns one JSON response.nodeandprivate(private models only, in place ofmessages): the chosen node's address and{ "enc", "ct", "clientKey" }as described on the Private inference page.ctis at most 1 MiB. Private requests must stream.
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].- For the image model, each image arrives as one chunk whose
delta.imagesis[{"type":"image_url","image_url":{"url":"data:image/png;base64,…"}}](about 1.15 MB); without streaming the images are inchoices[0].message.images. See Images. 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. Also a private request that carriesmessagesor lacksstream: true, and an open request that carriesprivateornode.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.500paywall_misconfigured: the paid route is not registered with the payment server. A deployment fault on Arcloak's side, never a buyer error; nothing was charged.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.503private_decrypt_failedorprivate_encrypt_failed: the node could not decrypt the request or could not encrypt its first chunk. Nothing was charged.private_encrypt_failed, a stream error event after the payment event: the node could not encrypt a later chunk. Only the delivered part was billed.private_required: reserved. A private body withmessagesor withoutstream: true, and an open body withprivateornode, answer400invalid_request; this code is not sent yet.attestation_failed: reserved. A node whose attestation stops verifying is kept as an open node, so a private request to it answers503node_unavailable; this code is not sent yet.
GET /v1/private/nodes
The attested nodes serving a base model now. Query model: a catalog model id. Response 200:
{"nodes":[{"address":"0x7d82…e487","enclaveKey":"0x…","attestation":{"kind":"dstack","measurement":"private-node-2026-09","verifiedAt":"2026-09-29T10:00:00.000Z","expiresAt":"2026-09-30T10:00:00.000Z"}}]}
nodes is empty when no attested node is online. kind is dstack for a hardware attestation, and such an entry also carries quote (the raw TDX quote, hex); dev never appears on the public API. model may also be given as private/<id>. Errors: 400 invalid_request for a missing or unknown model. The proxy does not rate-limit this route (only the chat and withdraw routes); a limit is a deploy-phase item.
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.