> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noise.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Choose a signing wallet, mint an API credential, and sign requests.

Authentication has two independent layers:

1. **Credential issuance.** A signature proves control of a wallet once, and returns an API key and secret.
2. **Request signing.** Every call carries an HMAC signature computed with that secret.

Orders add a third signature, from the wallet itself. That separation is deliberate: **a leaked API secret cannot forge orders or move funds**, because an order is valid only if it also carries the trading wallet's signature. The blast radius of a leaked secret is reads and cancels.

## Choosing a wallet

Every order is signed by the wallet that holds the collateral, because on-chain settlement checks that the order signature recovers to the trading wallet. The signing key must therefore be the key to the funds.

<Tabs>
  <Tab title="A dedicated wallet">
    Generate or use an EOA whose private key is already held locally, fund it with USDC on Base, and configure the client against it.

    This is the recommended setup. The wallet is separate from the Noise app account, so a compromise of the client is contained to the capital held in that wallet.
  </Tab>

  <Tab title="A Noise app wallet">
    The wallet in the Noise app is a [Privy](https://privy.io) embedded wallet, and Privy supports exporting its private key for use outside the app.

    Export it from **[Privy Home](https://home.privy.io)**: sign in with the login used for Noise, locate the Noise wallet, and export the key. The key is assembled in the browser on an origin controlled by neither Noise nor Privy, so neither party observes it.

    Once exported, the key is an ordinary EOA key and everything below applies unchanged. Because it is the same wallet the app account already trades with, it is already registered with the exchange and already holds the account's funds, so [Getting Tradeable](/trader-api/getting-tradeable) is largely a no-op for it.

    <Warning>
      Exporting moves the key out of a secure enclave and into whatever storage receives it. The
      operation cannot be reversed, and any party holding the key controls the funds, including the
      balance traded in the app. Store it in a secret manager, never in source control.

      A Noise app wallet is created owned solely by the account holder, with no Noise co-signer,
      which is what makes export possible without Noise's involvement. Accounts created before that
      became the default may hold a wallet Noise co-signs; Privy Home does not offer export for
      those, and a dedicated wallet is the alternative.
    </Warning>
  </Tab>
</Tabs>

## Minting a credential

Sign an EIP-712 `NoiseAuth` attestation with the wallet and `POST /keys`.

This route and `POST /keys/rotate` are the only authenticated routes that take **no** HMAC headers. The attestation signature is the authorization, which it must be, since no key exists yet.

The **domain**, where `chainId` is `exchange.attestationChainId` from `GET /status` (configured separately from the order chain, even when the two match):

```json theme={null}
{ "name": "NoiseAuth", "version": "1", "chainId": 8453 }
```

The `message` field must be exactly this string:

```
I authorize Noise to issue API credentials for this wallet. This signature does not authorize any transfer of funds.
```

| Field       | Type      | Notes                                                         |
| ----------- | --------- | ------------------------------------------------------------- |
| `address`   | `address` | The wallet being authorized                                   |
| `timestamp` | `string`  | Unix seconds, as a string. Must be within 300s of server time |
| `nonce`     | `uint256` | Start at `0`; increment only to rotate                        |
| `message`   | `string`  | The exact string above                                        |

```ts theme={null}
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.SIGNING_KEY as `0x${string}`);
const status = await fetch(`${BASE}/status`).then((r) => r.json());

const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = 0;

const signature = await account.signTypedData({
  domain: { name: "NoiseAuth", version: "1", chainId: status.exchange.attestationChainId },
  types: {
    NoiseAuth: [
      { name: "address", type: "address" },
      { name: "timestamp", type: "string" },
      { name: "nonce", type: "uint256" },
      { name: "message", type: "string" },
    ],
  },
  primaryType: "NoiseAuth",
  message: {
    address: account.address,
    timestamp,
    nonce: BigInt(nonce),
    message:
      "I authorize Noise to issue API credentials for this wallet. This signature does not authorize any transfer of funds.",
  },
});

const res = await fetch(`${BASE}/keys`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ address: account.address, timestamp, nonce, signature, scopes: ["trade"] }),
});
const { key, apiSecret, existing } = await res.json();
```

The response carries the key, the secret, and `existing`:

```json theme={null}
{
  "key": { "apiKey": "nk_a1b2c3...", "walletAddress": "0x...", "scopes": ["trade"], "status": "active", "attestationNonce": 0 },
  "apiSecret": "…",
  "existing": false
}
```

Issuance is **derive-or-create**: the secret is derived from the wallet, key, and nonce rather than stored, so repeating the call at the current nonce returns the same credentials with `existing: true`. A retry after a dropped response is therefore safe, and re-signing the same attestation recovers a lost secret.

<Warning>
  `apiSecret` is returned only by `POST /keys` and `POST /keys/rotate`. `GET /keys` lists metadata
  only, so treat the secret as write-once: store it in a secret manager at mint time rather than
  planning to retrieve it later.
</Warning>

### Scopes

| Scope   | Permits                                                      |
| ------- | ------------------------------------------------------------ |
| `read`  | Reading balances, positions, orders, trades, and market data |
| `trade` | Everything `read` permits, plus submitting and cancelling    |

`trade` implies `read`, because a client must reconcile its own fills. A `read` key suits accounting or dashboards; a `read` key that attempts to submit receives `403 FORBIDDEN`.

### Rotation and revocation

Rotate with `POST /keys/rotate` and a fresh attestation at **nonce + 1**. Rotating invalidates the old secret. When the current nonce is unknown, submit any nonce and read the `NONCE_CONFLICT` error, which names the nonce to use.

Revoke with `DELETE /keys/{apiKey}`. A revoked key also closes any open WebSocket with code `4001`.

### Restricting a key by source address

Both `POST /keys` and `POST /keys/rotate` accept an optional `ipAllowlist` of up to 20 entries, enforced on both HTTP and the WebSocket.

Matching is **exact**, so pass individual addresses, because a CIDR range is rejected rather than silently matching nothing. Omitting the field on rotate leaves an existing allowlist in place; passing an empty array clears it.

## Signing requests

Every authenticated request carries these headers:

| Header                | Value                                                             |
| --------------------- | ----------------------------------------------------------------- |
| `x-noise-api-key`     | The API key                                                       |
| `x-noise-timestamp`   | Unix seconds                                                      |
| `x-noise-signature`   | Hex HMAC-SHA256 of the canonical message, keyed by the API secret |
| `x-noise-recv-window` | Optional. Seconds of tolerance; default `5`, max `60`             |

The canonical message is four parts concatenated with **no separators**:

```
timestamp + METHOD + path + body
```

* **`timestamp`** is the same value as the header.
* **`METHOD`** is uppercased, for example `POST`.
* **`path`** includes the query string, for example `/api/trader/v1/account/orders?limit=50`.
* **`body`** is the raw request body exactly as sent, or an empty string for GET.

Sign the bytes actually transmitted. Re-serializing the body after signing is the most common cause of a `401`: an HTTP client that reorders keys or alters whitespace invalidates the signature.

```ts theme={null}
import { createHmac } from "node:crypto";

function sign(secret: string, method: string, path: string, body = "") {
  const timestamp = String(Math.floor(Date.now() / 1000));
  const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
  return { timestamp, signature: createHmac("sha256", secret).update(message).digest("hex") };
}

async function call(method: string, path: string, payload?: unknown) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const { timestamp, signature } = sign(API_SECRET, method, path, body);

  return fetch(`https://<host>${path}`, {
    method,
    headers: {
      "x-noise-api-key": API_KEY,
      "x-noise-timestamp": timestamp,
      "x-noise-signature": signature,
      ...(body && { "content-type": "application/json" }),
    },
    ...(body && { body }),
  });
}

await call("GET", "/api/trader/v1/account/balance");
```

<Info>
  Clock drift presents as a `401`. Where auth fails intermittently under load, check NTP on the
  calling machine before anything else, and raise `x-noise-recv-window` where latency to the API is
  genuinely high.
</Info>
