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:

  • 429 rate_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.
  • 502 bad_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.
  • 503 service_unavailable: the proxy refused the connection. Nothing was processed.
  • 504 gateway_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") and tier (small, mid or large for text, image for the image model).
  • price_usdc_per_million, with input and output as decimal USDC strings: the numbers on the Pricing page.
  • nodes_online: the nodes serving this model now.
  • private/<id> entries, each with private: 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, or private/<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: true for server-sent events; anything else returns one JSON response.
  • node and private (private models only, in place of messages): the chosen node's address and { "enc", "ct", "clientKey" } as described on the Private inference page. ct is at most 1 MiB. Private requests must stream.

Successful responses:

  • 200 with "stream": true: text/event-stream. Each data: line carries an OpenAI chat.completion.chunk. The last chunk has "finish_reason": "stop" and usage. Then comes {"object":"x402.payment","transaction":"0x…","amount":"3","network":"eip155:5042"}, then data: [DONE]. A failure after the stream started arrives as data: {"error":{"code":"…","message":"…"}} before [DONE].
  • For the image model, each image arrives as one chunk whose delta.images is [{"type":"image_url","image_url":{"url":"data:image/png;base64,…"}}] (about 1.15 MB); without streaming the images are in choices[0].message.images. See Images.
  • 200 with "stream": false: a chat.completion object with usage. The X-Payment-Transaction and PAYMENT-RESPONSE headers carry the settlement.

Errors:

  • 400 invalid_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 carries messages or lacks stream: true, and an open request that carries private or node.
  • 400 model_not_found: model is not in the catalog.
  • 402 payment_required: no payment was attached, or the attached one failed verification. The terms, and the reason on a failed verification, are in the PAYMENT-REQUIRED header.
  • 409 payment_in_use: the same payment authorization is already serving another request. Wait for it to finish and try again.
  • 500 paywall_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.
  • 402 settlement_failed, or the same code as a stream error event: the facilitator refused the settlement, so nothing was charged.
  • 502 settlement_error, or the same code as a stream error event: the settlement outcome is unknown. Check the payer's history before retrying.
  • 503 node_unavailable: no node serves the model, or the node stopped before its first token. The authorization is cancelled.
  • 503 unavailable: the job could not be recorded. Nothing was charged.
  • chunk_limit, a stream error event after the payment event: the node sent more than max_tokens allows. The answer was cut and only the delivered part was billed.
  • 503 private_decrypt_failed or private_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 with messages or without stream: true, and an open body with private or node, answer 400 invalid_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 answers 503 node_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:

  • 400 invalid_request: a malformed address or body.
  • 400 below_minimum: less than 1 USDC (1000000 units).
  • 401 bad_signature: the signature does not recover to the node address.
  • 404 not_found: the node never connected.
  • 409 stale_nonce: the nonce is not above last_nonce. Reload the node and use the next one.
  • 409 insufficient_balance: the amount is above the balance.
  • 503 wallet_below_floor, daily_cap or payout_busy: the payout wallet cannot pay right now. The balance is unchanged.
  • 502 transfer_unconfirmed: the transfer was sent but not confirmed. The withdrawal stays pending until it is reconciled; do not retry.
  • 502 transfer_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

  • 404 not_found: an unknown path.
  • 500 internal: an unexpected server error.