Arcloak

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") and tier (small, mid or large).
  • 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.

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: true for server-sent events; anything else returns one JSON response.

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].
  • 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.
  • 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.
  • 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.

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.