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

# Authentication

> Wallet-based authentication with ed25519 session keys

```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";

// Initialize SDK with wallet client (see Initialization guide)
const sdk = new MonacoSDK({
  walletClient,
  network: "staging",
  seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // public testnet RPC — use a private provider in production
});

// Frontend authentication (challenge-response flow)
await sdk.login(clientId);            // generates a session key, prompts the wallet once
await sdk.login(clientId, options);   // with optional configuration
await sdk.logout();                   // revoke session + clear state
await sdk.refreshAuth();              // extend the current session's expiry

// Session management (low-level, on sdk.auth)
await sdk.auth.refreshSession();
await sdk.auth.revokeSession();

// State checks
sdk.isAuthenticated();
sdk.getAuthState();
sdk.setAuthState(authState);
```

Authentication is wallet-authorized session-key signing: the wallet signs once to authorize an ed25519 session key, and the SDK then signs every request with that key. There are no access or refresh tokens.

## Frontend Authentication

<ResponseField name="login" type="(clientId: string, options?: { connectWebSocket?: boolean }) => Promise<AuthState>">
  Complete authentication flow: generates a session keypair, creates a challenge that commits to the session public key, signs it with the wallet, verifies it, and wires the session into every SDK module.

  **Parameters:**

  * `clientId` (required): Your application's public client ID
  * `options` (optional):
    * `connectWebSocket`: Auto-connect authenticated WebSocket channels after login (default: `false`). Public channels (OHLCV, Orderbook) never require authentication.

  **Returns:** an `AuthState`:

  * `sessionPublicKey`: 64-char hex ed25519 session public key registered with the server
  * `sessionPrivateKey`: 64-char hex ed25519 session private key used to sign requests — **the credential; keep it on the client**
  * `expiresAt`: Unix timestamp (seconds) when the session expires
  * `user`: user object with `id`, `address`, `username`
  * `isNewUser` (optional): `true` when this sign-in created the user — the wallet's first sign-in to this application. Gate first-time UX on it (e.g. show an optional "have a referral code?" input only to new users).
  * `referralApplied` (optional): `true` when a TraderCodes referral was recorded on this sign-in (a valid TraderCode was applied). Use it to confirm the referral landed. See [TraderCodes](/developers/tradercodes).

  After `login()`, all REST and WebSocket calls are signed automatically — there is no token to pass.

  ```typescript theme={null}
  // Auto-connect authenticated WebSocket channels
  const authState = await sdk.login(clientId, { connectWebSocket: true });
  const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
  const unsubscribe = sdk.ws.orders(pair.id, "SPOT", callback);

  // Or connect the WebSocket later
  const authState = await sdk.login(clientId);
  await sdk.ws.connect();

  // Log out: revokes the session and disconnects authenticated sockets
  await sdk.logout();
  ```
</ResponseField>

<ResponseField name="logout" type="() => Promise<void>">
  Revoke the current session on the server, disconnect authenticated WebSocket channels, and clear local auth state.
</ResponseField>

<ResponseField name="refreshAuth" type="() => Promise<AuthState>">
  Extend the current session's expiry by signing a refresh request with the active session key. The keypair is unchanged; the returned `AuthState` has the updated `expiresAt`. Throws if there is no active session or it has expired/been revoked.
</ResponseField>

## Request Signing Model

The SDK signs authenticated requests automatically. Monaco accepts two signing protocols over the same session key, and `SDKConfig.requestSigning` selects which one the SDK produces — `"legacy"` by default, or `"rfc9421"`. Never send both credential sets on one request: `Signature` or `Signature-Input` alongside any `X-Monaco-*` header is rejected with `401`. `Content-Digest` is not a credential — it is ordinary integrity metadata, so it may accompany a legacy-signed request without selecting RFC verification.

### Legacy signing (default)

Direct REST clients send three headers:

| Header | Value |
| - | - |
| `X-Monaco-PublicKey` | 64-character lowercase-hex ed25519 session public key |
| `X-Monaco-Timestamp` | Unix timestamp in milliseconds, within 30 seconds of server time |
| `X-Monaco-Signature` | Hex ed25519 signature over the signing string |

The signing string is:

```text theme={null}
METHOD\npath?query\ntimestamp_ms\nSHA256_hex(body)
```

### RFC 9421 signing (opt-in)

Set `requestSigning: "rfc9421"` when constructing the SDK and it sends `Signature-Input`, `Signature`, and `Content-Digest` instead, covering `@method`, `@path`, `@query`, and the body digest. There is no automatic downgrade, so opt in only against a gateway that verifies it. See [HTTP message signatures](/reference/http-message-signatures) for the full profile and the migration sequence.

Either way the body hash is signed, so a captured request cannot be reused with a modified body, and the timestamp bounds how long it stays valid at all. Freshness is an anti-stale control, not an anti-replay one: an identical signed request can still be accepted again inside the 30-second window, and Monaco keeps no nonce or replay cache. Where an endpoint takes an `idempotencyKey` — order creation and batch creation do — treat it as required rather than optional on any retry, and reuse the original key and payload. An endpoint without one carries no exactly-once guarantee. Use the SDK unless you need a custom REST client.

## Low-Level Operations

These are the building blocks `login()` runs internally. Most applications should call `login()` and never touch them directly.

<ResponseField name="auth.createChallenge" type="(address: string, clientId: string, sessionPublicKey: string) => Promise<ChallengeResponse>">
  Create a single-use challenge whose message embeds the supplied session public key.

  **Returns:** `nonce`, `message` (to sign with the wallet), `expiresAt`.

  The challenge is single-use and authorizes exactly one keypair, once. It lives 5 minutes, or 30 minutes on a deployment where contract-signature login is enabled — a multisig has to collect owner approvals before it can answer. That window is uniform for every address rather than eligibility-specific, so this unauthenticated endpoint cannot be used to probe which addresses hold a BuilderCodes payout bucket. Read `expiresAt` rather than assuming either value.

  Shares one per-client-address rate budget with `verifySignature` — 60 requests/min sustained, burst 120 by **default**, both overridable per deployment via `AUTH_RATE_LIMIT_PER_MINUTE` and `AUTH_RATE_LIMIT_BURST` — so a login costs two. Pace against the environment's own budget rather than these figures. **Enforcement is itself gated**: until `auth_rate_limit_enforced` is turned on for the deployment — it defaults off — an over-budget request is admitted with a warn log rather than rejected, so do not treat the absence of a `429` as proof you are inside the budget. Over budget is `429` with `details.retryAfter`; see [rate limits](/developers/rate-limits#authentication).

  **Other outcomes:** `400` for invalid request parameters, `403` when the origin is not allowed for this application, `404` for an invalid `clientId`, and `500` for an internal failure.
</ResponseField>

<ResponseField name="auth.signChallenge" type="(message: string) => Promise<string>">
  Ask the connected wallet to authorize a challenge message. Returns a hex signature. An EOA signs with EIP-191 `personal_sign`; a smart-contract wallet such as a Safe has no private key and returns a signature its own contract accepts, which the server checks under EIP-1271. Submit the returned value unchanged, and do not assume the 65-byte EOA shape — a contract signature is usually longer. Server-side EIP-1271 verification is deliberately narrow: it is offered only to a wallet holding a BuilderCodes payout bucket, on a deployment with the contract-wallet flag on and a chain RPC endpoint configured. An ordinary smart-contract trading account is NOT covered — it still takes the recovery path and cannot authenticate.
</ResponseField>

<ResponseField name="auth.verifySignature" type="(address, signature, nonce, clientId, session: SessionCredentials, referralCode?: string) => Promise<AuthState>">
  Verify the wallet signature against the challenge and register the session public key, returning a new `AuthState`. `session` is the locally-generated `{ publicKey, privateKey }` keypair whose public key was bound in `createChallenge`.

  Re-running verify with a session public key that is already registered returns `409 Conflict` ("resource already exists"). `login()` generates a fresh keypair on each call, so this only affects manual low-level flows that reuse a key — generate a new session keypair instead of retrying.

  A rejected signature is `401` — including a wallet contract that rejects by reverting, which is the wallet answering "no". `signature` is bounded at 8192 decoded bytes (16386 characters with the `0x` prefix); anything longer is rejected as invalid rather than forwarded to the chain. The bound is far above any realistic owner set and EOA signatures never approach it.

  A `503` is **not** a verdict on the signature. Deciding how to verify an address eligible for contract-signature login means reading the chain, and a `503` covers three cases: the chain did not answer, it did not answer within the deadline, or the server was at its concurrent-verification capacity and declined to ask. It is retryable, and an eligible address can see it even when it turns out to be an EOA, because the code lookup precedes the choice of verification path. Addresses that are not eligible never reach the chain.

  A fourth trigger has nothing to do with contract wallets: verify reads the private-beta feature-flag state before anything else, and a read that cannot be served surfaces as the same retryable `503`. **Any** address can see that one, ahead of any signature or eligibility work — so do not diagnose every `503` here as a chain or verifier problem.

  Shares the authentication rate budget with `createChallenge`; over budget is `429`, checked **before** the signature, so a throttled request is never an authentication failure.

  **Other outcomes:** `400` for invalid request parameters, or a nonce that has expired or already been used; `403` when the origin is not allowed for this application, or with `serverCode` `ACCOUNT_DEACTIVATED` when the signing account is deactivated (any signed request can return it, and signing in again does not help); `404` for an invalid nonce or `clientId`; and `500` for an internal failure.
</ResponseField>

<ResponseField name="auth.refreshSession" type="() => Promise<SessionRefreshResponse>">
  Extend the active session's expiry (signed with the session key). Returns the new `expiresAt`. `sdk.refreshAuth()` wraps this and also updates the local `AuthState`.
</ResponseField>

<ResponseField name="auth.revokeSession" type="() => Promise<void>">
  Revoke the active session on the server (signed with the session key). `sdk.logout()` calls this for you.
</ResponseField>

## State Checks

<ResponseField name="isAuthenticated" type="() => boolean">
  Whether a session is currently active.
</ResponseField>

<ResponseField name="getAuthState" type="() => AuthState | undefined">
  The current `AuthState`, or `undefined` if not authenticated.
</ResponseField>

<ResponseField name="setAuthState" type="(authState: AuthState) => void">
  Set the auth state directly and propagate the session key into every API module. Use it to restore a persisted session (avoiding another wallet prompt) or to share a session across SDK instances. Does not connect the WebSocket or make any API call.

  ```typescript theme={null}
  // Restore a persisted session
  const saved = JSON.parse(localStorage.getItem('monaco_session'));
  if (saved && saved.expiresAt * 1000 > Date.now()) {
    sdk.setAuthState(saved);
  } else {
    await sdk.login(clientId);
  }
  ```

  <Warning>
    The `sessionPrivateKey` inside `AuthState` is the credential. Storing it in `localStorage` exposes it to XSS. Prefer the most secure storage your platform offers, scope it to your origin with a Content Security Policy, and clear it on logout. For React, the [`useAuth`](/sdk/react/hooks/use-auth) hook manages persistence for you.
  </Warning>
</ResponseField>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.