API docs

Noid AI speaks the OpenAI chat completions API. Any client that lets you set a base URL and an API key works: chat apps, agent harnesses, the official SDKs. Make a key under Account, API keys in the app.

Base URL and key

Base URL
https://api.noidai.chat/v1
Auth
Authorization: Bearer nb_...

Keys start with nb_ and hold credits. The API calls credits questions: one credit is one questions unit. Every request made with a key can be linked to other requests with the same key (never to your wallet), so use one key per app or task.

Endpoints

  • GET/v1/modelsNo authModels with label, privacy level and cost in credits per request.
  • POST/v1/chat/completionsBearer keyOpenAI chat completions, streaming supported. Deducts the model's cost first; refunds it if the provider fails before any output.
  • GET/v1/keys/balanceBearer keyCredits left on the key, and the hour it was last used for a chat request.
  • POST/v1/keys/transferBearer keyMove credits from this key to another key (by its hash). Credits never go back into a pack.
  • DELETE/v1/keysBearer keyRevoke the key. Refused with key_not_empty while it holds credits.
  • POST/v1/keysNo authLoad credits onto the key with this hash (new or existing): 10 per proof.
  • GET/v1/membersNo authEvery credit pack fingerprint in order. Check it against the contract and compute the root and your path yourself.
  • GET/v1/accountBearer account tokenThe encrypted account blob and its version; 404 if none.
  • PUT/v1/accountBearer account tokenSave the encrypted account as the next version (conditional write).

Examples

curl https://api.noidai.chat/v1/chat/completions \
  -H "Authorization: Bearer $NOID_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model id>",
    "messages": [{ "role": "user", "content": "Hello" }],
    "stream": true
  }'
curl https://api.noidai.chat/v1/keys/balance \
  -H "Authorization: Bearer $NOID_KEY"
# {"questions": <credits left on the key>, "lastUsedAt": "<hour, ISO>" | null}
# Move 100 credits from this key to another key of yours
curl https://api.noidai.chat/v1/keys/transfer \
  -H "Authorization: Bearer $NOID_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "toKeyHash": "0x<sha256 of the other key>", "questions": 100 }'
# {"questions": <credits left on this key>}

# Revoke a key (only once its balance is 0)
curl -X DELETE https://api.noidai.chat/v1/keys -H "Authorization: Bearer $NOID_KEY"
# 204, or 409 key_not_empty

OpenAI SDK

JavaScript or TypeScript (package openai):

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.noidai.chat/v1",
  apiKey: process.env.NOID_KEY, // nb_...
});

const stream = await client.chat.completions.create({
  model: "<model id>",
  messages: [{ role: "user", content: "Hello" }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Python (package openai):

from openai import OpenAI
import os

client = OpenAI(base_url="https://api.noidai.chat/v1", api_key=os.environ["NOID_KEY"])

reply = client.chat.completions.create(
    model="<model id>",
    messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)

Models

Private enclave models run inside a secure enclave (a trusted execution environment) at the provider. Standard models are served normally, so the provider can read the prompt. Neither is told who asked; both read the full prompt. Use the id in the model field.

Loading models

Errors

Error bodies use the OpenAI shape: { "error": { "message", "type", "code" } }. Branch on code; messages may change.

  • 400invalid_requestThe body or query failed validation, or is not JSON.
  • 413request_too_largeThe body is larger than the gateway accepts.
  • 400context_length_exceededThe messages are longer than the model's input cap.
  • 400provider_rejectedThe model provider refused the request.
  • 400invalid_ticketA proof, its signal x or its externalNullifier is wrong.
  • 409stale_rootA proof was made against a root that is no longer recent. Fetch and check /v1/members again, then prove the same slots with the same nonce and key hash.
  • 409ticket_usedThese exact proofs were credited before (for example, an earlier send whose response was lost).
  • 403member_slashedA slot was reused with a different signal: the pack's secret is exposed and the pack is slashed.
  • 401invalid_account_tokenThe account token is missing or malformed, or no account is stored under it.
  • 409account_conflictAnother device saved the account first. Fetch it, merge, and save again with the next version.
  • 409key_not_emptyThe key still has credits. Move them to another key before revoking it.
  • 403key_revokedThe key was revoked and can no longer be used or topped up.
  • 401invalid_api_keyThe key is missing, mistyped or unknown.
  • 402insufficient_questionsThe key has fewer credits left than the model costs.
  • 404model_not_foundThe model id is not served here.
  • 404not_foundNo such route.
  • 429provider_rate_limitedThe model provider is rate limiting the gateway.
  • 499client_closedThe client left before the answer started; no refund.
  • 500internal_errorThe gateway failed. Details stay in its own log.
  • 502provider_errorThe model provider failed. If no output was sent, the credits are refunded.
  • 502provider_unreachableThe model provider could not be reached. The credits are refunded.
  • 503chain_unavailableThe gateway's chain index is unavailable; nothing was credited.
  • 503gateway_busyProof verification is at capacity; nothing was credited. Send the same request again later.
  • 503not_readyReadiness check failed (database down).

Loading credits yourself

The app does this for you. For anyone building their own client: a credit pack is a number of slots bought on-chain against a secret that never leaves your device. Each slot is worth 10 credits and is spent with an RLN proof (Rate-Limiting Nullifier, Groth16 on BN254), which shows that its sender holds a pack in the member tree without saying which one. The API calls these proofs tickets.

  1. Fetch /v1/members and check the leaves against the contract through your own RPC: fold them with acc = keccak256(abi.encode(acc, leaf)) from 32 zero bytes and compare with accumulatorAt(leaves.length). Then build the tree (depth 20, Poseidon) and compute the root and your own path. Never ask anyone for your path: that would reveal your leaf. Check on a timer, not right before spending, or your RPC sees each spend.
  2. Make the key yourself (nb_ plus 32 random bytes, base64url) and keep it. Pick unused slots (0 to pack size minus 1, at random) and a fresh random 32-byte nonce. Proof i of the request proves the signal x = keccak256("noid-ticket-v2", nonce, keyHash, uint32(i)) >> 8, so it can only ever credit that key.
  3. Send the nonce, the key hash and 1 to 100 proofs to /v1/keys. Nothing else: no key, wallet, leaf or path. A request is all or nothing.

Each slot can be spent once, ever. Two proofs for the same slot with different signals reveal the secret, and the gateway slashes the pack. Resending the same request is safe: if the first one went through, the answer is ticket_used and /v1/keys/balance shows the credit.

curl https://api.noidai.chat/v1/members
# {"chainId": 11155111, "contract": "0x...", "depth": 20,
#  "leaves": ["<decimal>", "<decimal>", ...]}
# Check: keccak chain over the leaves == contract.accumulatorAt(leaves.length)
# key     = "nb_" + 32 random bytes, base64url (made by you, never sent)
# keyHash = sha256(utf8(key))
curl https://api.noidai.chat/v1/keys \
  -H "Content-Type: application/json" \
  -d '{
    "nonce": "0x<32 random bytes, hex>",
    "keyHash": "0x<sha256 of the key, hex>",
    "tickets": [
      { "proof": { "pi_a": [...], "pi_b": [...], "pi_c": [...],
                   "protocol": "groth16", "curve": "bn128" },
        "publicSignals": { "y": "...", "root": "...", "nullifier": "...",
                           "x": "...", "externalNullifier": "..." } }
    ]
  }'
# {"questions": <the key's new total>}
# The same call credits a new key or tops up an existing one.

Account sync

The web app keeps its account (pack secrets, chats, keys) as one document, encrypted in the browser with AES-256-GCM and stored by the gateway under the hash of an account token. Token and key come from HKDF over keccak256 of the wallet's signature of a fixed sign-in message. The gateway never sees the wallet. Writes are versioned: a PUT succeeds only for the stored version plus one. The full spec is in docs/design/protocol.md, section Account (client).

curl https://api.noidai.chat/v1/account -H "Authorization: Bearer acct_..."
# {"version": 7, "blob": "<base64 of iv || AES-GCM ciphertext>"}  or 404

curl -X PUT https://api.noidai.chat/v1/account \
  -H "Authorization: Bearer acct_..." -H "Content-Type: application/json" \
  -d '{ "version": 8, "blob": "<base64>" }'
# {"version": 8}, or 409 account_conflict: fetch, merge, retry