# Authentication (/docs/guides/authentication) Every request to carries an API key in the `Authorization` header. Keys spend the credit of the account that created them. This guide covers the key lifecycle from creation to revocation. ## Create a key [#create-a-key] 1. Sign in at [drex.nace.ai/login](https://drex.nace.ai/login) and open **API Keys**. 2. Click **Create key**. 3. Enter a name, such as the app or environment that will use the key, and click **Create key**. 4. Copy the key from the dialog. The full key appears once. After you close the dialog, the **API Keys** page shows only the key's prefix. If you lose a key, revoke it and create a new one. A key looks like `nace_sk_` followed by 43 URL-safe characters. An account can have at most 3 active keys. When you reach the limit, **Create key** is disabled until you revoke one. ## Store the key [#store-the-key] Put the key in an environment variable or a secret manager. Do not commit it to source control, and do not ship it in code that runs in a browser or a mobile app. ```bash export DREX_API_KEY="nace_sk_..." ``` The TypeSafe SDK reads the key from `TYPESAFE_API_KEY` when you do not pass `apiKey`. See [Migrate from TypeSafe](/docs/guides/migrate-from-typesafe) for the full environment setup. ## Send the key [#send-the-key] Send the key as a Bearer token on every request. ```bash curl https://drex.nace.ai/v1/models \ -H "Authorization: Bearer $DREX_API_KEY" ``` ```ts import { TypeSafeClient } from "@typesafe-ai/sdk"; const client = new TypeSafeClient({ baseURL: "https://drex.nace.ai", apiKey: process.env.DREX_API_KEY, defaultModel: "drex-v1.0", }); ``` ```python import os, httpx headers = {"Authorization": f"Bearer {os.environ['DREX_API_KEY']}"} response = httpx.get("https://drex.nace.ai/v1/models", headers=headers) ``` ## Keep the key on the server [#keep-the-key-on-the-server] Only call Drex from code you control on a server. Anyone who loads a page can read a key shipped in its JavaScript. The TypeSafe SDK enforces this. If `TypeSafeClient` is constructed in a browser, the constructor throws a `TypeSafeError`: > TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk. Pass `dangerouslyAllowBrowser: true` only in a context where exposing the key is acceptable, such as a local demo. For a web app, call Drex from your own backend and have the browser call your backend. ## Rotate a key [#rotate-a-key] To rotate a key without downtime: 1. On **API Keys**, click **Create key** and create the replacement. If you already have 3 active keys, revoke an unused one first. 2. Deploy the new key to your servers and confirm that requests succeed. 3. Click **Revoke** next to the old key, then confirm with **Revoke key**. Requests with the revoked key stop working within 5 seconds. Revocation cannot be undone. ## Revoke a key [#revoke-a-key] To revoke a key you no longer need, or one that may have leaked, open **API Keys**, click **Revoke** on that row, and confirm with **Revoke key**. A revoked key stays in the list with the status **Revoked** so you can see when it was last used. ## What a bad key returns [#what-a-bad-key-returns] A missing, malformed, or revoked key returns `401` with the type `authentication_error`. This is a real capture from `curl -i https://drex.nace.ai/v1/models` with no `Authorization` header. The request id varies per call. ```http HTTP/2 401 x-request-id: req_7c1e4a9b2d3f4e5a8b6c7d8e9f0a1b2c x-typesafe-request-id: req_7c1e4a9b2d3f4e5a8b6c7d8e9f0a1b2c access-control-expose-headers: retry-after, retry-after-ms, x-request-id, x-typesafe-request-id { "error": { "type": "authentication_error", "message": "Invalid API key. Pass a nace_sk_ key as `Authorization: Bearer `." }, "request_id": "req_7c1e4a9b2d3f4e5a8b6c7d8e9f0a1b2c" } ``` The TypeSafe SDK raises this as an `AuthenticationError` and does not retry it. Every Drex response carries the same request id in the body and in both headers. Quote it when you contact support. Other error types are listed in [Errors](/docs/reference/errors). ## Check a key without spending credit [#check-a-key-without-spending-credit] `GET /v1/models` needs a valid key but works when the account has no credit, so you can use it to confirm that a key is active before you deploy it. ```bash curl https://drex.nace.ai/v1/models \ -H "Authorization: Bearer $DREX_API_KEY" ``` ```json title="Response" { "models": [ { "name": "drex-v1.0", "description": "Drex System One decision model. Typed questions in, calibrated probabilities out.", "release_date": "2026-09-24", "alias_for": null }, { "name": "drex-v1.5", "description": "Drex 1.5 decision model. Typed questions in, calibrated probabilities out; states up to 131,072 tokens.", "release_date": "2026-09-28", "alias_for": null }, { "name": "drex-latest", "description": "Points to drex-v1.5 (Drex 1.5). Moves to a newer version only on an announced date.", "release_date": "2026-09-28", "alias_for": "drex-v1.5" }, { "name": "drex-v1.1", "description": "Retired on 2026-09-28. Requests that name it are served by drex-v1.5 (Drex 1.5) at its price.", "release_date": "2026-09-25", "alias_for": "drex-v1.5" } ], "request_id": "req_0a2b4c6d8e1f4a3b9c5d7e9f1a3b5c7d" } ``` A `POST /v1/systemone` call with a valid key and no credit returns `402` with the type `insufficient_credit`. Top up on **Billing** to keep going. See [Pricing](/docs/reference/pricing). # Errors and retries (/docs/guides/errors-and-retries) Drex returns one error envelope for every failure, with a `type` that tells you whether to retry. This guide shows how to classify each error, how long to wait, and how to write the retry loop in the TypeSafe SDK or by hand. For every status, message, and header, see the [error reference](/docs/reference/errors). ## Retry or fix [#retry-or-fix] Every error response has this body. `issues` appears only on 422, and `request_id` is always present. ```json title="Error envelope" { "error": { "type": "rate_limit_error", "message": "Too many concurrent requests for this account." }, "request_id": "req_<32 hex characters>" } ``` Decide by `type` or by HTTP status. | HTTP | `error.type` | What happened | Do this | | ----------- | ------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------- | | 429 | `rate_limit_error` | Your account is over its requests per minute or its in-flight limit | Wait `retry-after-ms`, then retry | | 529 | `overloaded` | Drex is at capacity, the model timed out, or Drex is paused | Wait `retry-after-ms`, then retry | | 500 | `internal_error` | Something failed inside Drex | Retry with backoff | | No response | Network error or timeout | The request did not complete | Retry with backoff | | 401 | `authentication_error` | Missing or invalid `nace_sk_` key | Fix the key. See [Authentication](/docs/guides/authentication) | | 402 | `insufficient_credit` | The account has no spendable credit | Top up in the dashboard | | 422 | `invalid_request_error` | The body failed validation or is over a limit | Fix the body. `error.issues` says where | A 4xx that is not 429 will fail the same way on every retry. Retrying it spends your rate limit and delays the real fix. ## Honor retry-after [#honor-retry-after] A 429 or 529 carries two headers with the same value: `retry-after` in whole seconds, rounded up, and `retry-after-ms` in milliseconds. Read `retry-after-ms` when you can. Waiting shorter than the header will hit the same limit again. The values Drex sends: | Cause | `error.type` | `retry-after-ms` | | ------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------ | | Too many concurrent requests for this account | `rate_limit_error` | 1000 | | Requests per minute exceeded for this account | `rate_limit_error` | Time until the oldest request leaves the 60-second window, at least 1000 | | Drex is at capacity, or its rate limiter is unavailable | `overloaded` | 2000 | | The model timed out or was unavailable | `overloaded` | 2000 | | Drex is paused | `overloaded` | 30000 | The in-flight limit is the one most clients hit first. A free account may have 2 requests in flight, a paid account 16. If you send a batch of tickets with `Promise.all` or a thread pool, cap the concurrency at your tier's limit instead of retrying your way through 429s. Limits are on the [limits reference](/docs/reference/limits). ## Set the client timeout to at least 60 seconds [#set-the-client-timeout-to-at-least-60-seconds] Drex waits up to 55 seconds for the model before it returns a 529, and the route itself stops at 60 seconds. Under load a request can sit in the model's queue for most of that window while capacity is added. A client timeout below 60 seconds abandons requests that were about to succeed, and the retry joins the queue again at the back. The TypeSafe SDK's default timeout is 10,000 ms per attempt. Raise it. ## Configure the TypeSafe SDK [#configure-the-typesafe-sdk] The SDK retries on its own. Its defaults, from `RetryPolicy`: * `maxRetries` is 2, so a request is attempted at most 3 times. * Backoff starts at 500 ms, doubles per attempt, and is capped at 5,000 ms, with up to 25% subtracted as jitter. * It retries HTTP 408, 429, and every 5xx, which includes 529. It also retries connection errors and timeouts. * It honors `retry-after-ms` and `retry-after` up to 60,000 ms. Longer server delays fall back to backoff. Point it at Drex and give it the longer timeout: ```ts title="client.ts" import { TypeSafeClient } from "@typesafe-ai/sdk"; export const client = new TypeSafeClient({ apiKey: process.env.DREX_API_KEY, baseURL: "https://drex.nace.ai", defaultModel: "drex-v1.0", timeout: 60_000, retry: { maxRetries: 4, backoffMaxMs: 10_000 }, }); ``` Each HTTP status maps to an error class, so you can tell a fix from a retry that ran out: ```ts title="handle-errors.ts" import { APIConnectionError, APIError, AuthenticationError, RateLimitError, UnprocessableEntityError, noul, } from "@typesafe-ai/sdk"; import { client } from "./client"; try { const { answers } = await client.systemOne({ state: "I was charged twice for my March invoice.", questions: { wants_refund: noul("Is the customer asking for a refund?") }, }); console.log(answers.wants_refund.noul); } catch (error) { if (error instanceof UnprocessableEntityError) { // Fix the request. error.body holds the envelope with error.issues. console.error("invalid request", error.requestId, error.body); } else if (error instanceof AuthenticationError) { console.error("check DREX_API_KEY", error.requestId); } else if (error instanceof RateLimitError) { // Retries are used up. error.retryAfterMs is the last server hint. console.error("rate limited", error.requestId, error.retryAfterMs); } else if (error instanceof APIError) { // 402 arrives here as a plain APIError with status 402. console.error(`Drex ${error.status}`, error.requestId, error.message); } else if (error instanceof APIConnectionError) { console.error("network or timeout after retries", error.message); } else { throw error; } } ``` `error.requestId` comes from the `x-typesafe-request-id` header, which Drex sets on every response. See [Migrate from TypeSafe](/docs/guides/migrate-from-typesafe) for the environment variables that do the same as the constructor options. ## Write your own retry loop [#write-your-own-retry-loop] Without the SDK, retry 429, 529, 500, and network failures. Use the server's `retry-after-ms` when present, and capped exponential backoff with jitter otherwise. Stop after a fixed number of attempts, and never retry 401, 402, or 422. ```ts title="drex.ts" const RETRYABLE = new Set([429, 500, 529]); export async function systemOne(body: unknown, maxRetries = 4): Promise { for (let attempt = 0; ; attempt++) { let response: Response | undefined; try { response = await fetch("https://drex.nace.ai/v1/systemone", { method: "POST", headers: { authorization: `Bearer ${process.env.DREX_API_KEY}`, "content-type": "application/json", }, body: JSON.stringify(body), signal: AbortSignal.timeout(60_000), }); } catch (error) { if (attempt >= maxRetries) throw error; } if (response) { if (response.ok) return response.json(); const requestId = response.headers.get("x-request-id"); if (!RETRYABLE.has(response.status) || attempt >= maxRetries) { throw new Error(`Drex ${response.status} (${requestId}): ${await response.text()}`); } console.warn(`Drex ${response.status} (${requestId}), retrying`); } await sleep(retryDelayMs(attempt, response?.headers)); } } function retryDelayMs(attempt: number, headers?: Headers): number { const serverMs = Number(headers?.get("retry-after-ms")); if (serverMs > 0) return serverMs; const capped = Math.min(500 * 2 ** attempt, 10_000); return capped / 2 + Math.random() * (capped / 2); } const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); ``` ```python title="drex.py" import os import random import time import httpx RETRYABLE = {429, 500, 529} def system_one(body: dict, max_retries: int = 4) -> dict: with httpx.Client(timeout=60.0) as client: attempt = 0 while True: headers = None try: response = client.post( "https://drex.nace.ai/v1/systemone", headers={"Authorization": f"Bearer {os.environ['DREX_API_KEY']}"}, json=body, ) except httpx.TransportError: if attempt >= max_retries: raise else: if response.is_success: return response.json() request_id = response.headers.get("x-request-id") if response.status_code not in RETRYABLE or attempt >= max_retries: raise RuntimeError(f"Drex {response.status_code} ({request_id}): {response.text}") print(f"Drex {response.status_code} ({request_id}), retrying") headers = response.headers time.sleep(retry_delay_seconds(attempt, headers)) attempt += 1 def retry_delay_seconds(attempt: int, headers: httpx.Headers | None) -> float: server_ms = headers.get("retry-after-ms") if headers is not None else None if server_ms and server_ms.isdigit() and int(server_ms) > 0: return int(server_ms) / 1000 capped = min(0.5 * 2**attempt, 10.0) return capped / 2 + random.random() * capped / 2 ``` `httpx.TimeoutException` is a subclass of `httpx.TransportError`, so the Python loop retries timeouts too. In both loops the jitter keeps a fleet of clients that hit a 529 at the same moment from retrying at the same moment. ## Log and report request\_id [#log-and-report-request_id] Every response, success or failure, carries the same id in three places: the `request_id` field of the body and the `x-request-id` and `x-typesafe-request-id` headers. Log it next to your own correlation id on every call. If a request fails in a way this page does not explain, or a 500 or 529 keeps coming back, write to [support@nace.ai](mailto:support@nace.ai) with the `request_id` and the time. Drex joins every log line for a request on that id, so it is the fastest way to a cause. ## Know what a retry costs [#know-what-a-retry-costs] Drex debits your account only after it has sent a successful response. The debit runs after the response is on the wire and records `usage.input_tokens` for that `request_id`. A 401, 402, 422, 429, 500, or 529 is not billed, so retrying a failure does not cost credit. A 429 is decided before Drex reads the body, so a rate-limited request costs nothing but the round trip. Every successful response is billed, including duplicates. Do not hedge by sending the same request twice in parallel: if both succeed, you pay for both. Usage by request is on the dashboard's **Usage** page. # Migrate from TypeSafe (/docs/guides/migrate-from-typesafe) Drex is wire-compatible with TypeSafe's Jev. The `@typesafe-ai/sdk` package works against without a fork. This guide covers the configuration change, the requests that Drex rejects, and a check that the switch worked. It is written against SDK version 0.6.0. ## Change three environment variables [#change-three-environment-variables] The SDK reads its base URL, key, and default model from the environment when the constructor does not set them. ```dotenv title=".env" TYPESAFE_BASE_URL=https://drex.nace.ai TYPESAFE_API_KEY=nace_sk_... TYPESAFE_DEFAULT_MODEL=drex-v1.0 ``` If your code sets these in the constructor instead, change them there. Explicit options take precedence over environment variables. ```ts title="Before" import { TypeSafeClient } from "@typesafe-ai/sdk"; const client = new TypeSafeClient({ apiKey: process.env.TYPESAFE_API_KEY, }); ``` ```ts title="After" import { TypeSafeClient } from "@typesafe-ai/sdk"; const client = new TypeSafeClient({ baseURL: "https://drex.nace.ai", apiKey: process.env.DREX_API_KEY, defaultModel: "drex-v1.0", timeout: 60_000, }); ``` Set `defaultModel` in one of the two places. Without it, the SDK sends `jev-latest`, and Drex returns `422`. The `timeout` line is explained under [Raise the SDK timeout](#raise-the-sdk-timeout). ## Verify the switch [#verify-the-switch] List the models. The SDK unwraps the `{ "models": [...] }` envelope and returns the array. ```ts const models = await client.models.list(); console.log(models.map((m) => m.name)); // [ 'drex-v1.0', 'drex-v1.5', 'drex-latest', 'drex-v1.1' ] ``` The list has each pinned version, then the `drex-latest` alias, then retired ids such as `drex-v1.1`, which `drex-v1.5` now answers. See [Models](/docs/reference/models#versions-and-the-drex-latest-alias). `GET /v1/models` needs a valid key but works with zero credit, so this check does not spend anything. A `401` here means the key is wrong. See [Authentication](/docs/guides/authentication). ## What changes [#what-changes] Work through this list before you send production traffic. Each item is a request that TypeSafe accepted and Drex rejects, or a behavior that differs. ### Send a `drex-*` model name [#send-a-drex--model-name] Drex accepts pinned versions such as `drex-v1.0`, the `drex-latest` alias, any other `drex-*` name, and legacy `nacedm-*` names. Anything else returns `422`, so a half-migrated client fails on the first call instead of silently using the wrong model. A request with `"model": "jev-latest"` returns this issue: ```json { "path": "model", "message": "unknown model \"jev-latest\"; use \"drex-v1.0\", \"drex-v1.5\" or \"drex-latest\"" } ``` Search your code for hard-coded `jev-` model names and for calls that pass `model` per request. ### Pass `instructions` as a string [#pass-instructions-as-a-string] Drex requires `instructions` to be a string when present. The SDK types allow a JSON object, an array, or `null`, and `noul()` with no arguments sends `instructions: null`. Drex rejects both: ```json { "path": "questions.a.instructions", "message": "must be a string" } ``` Replace `noul()` and `noul(null, criteria)` with `noul("Your question as text", criteria)`. Replace JSON instructions with text. ### Pass criteria descriptions as strings [#pass-criteria-descriptions-as-strings] The SDK types allow a JSON object or array as any criteria description. Drex does not. * `choice`: each label's description must be a string or `null`. A JSON description returns the issue `must be a string or null` at `questions..criteria.