Skip to main content
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

(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 PitPass referral was recorded on this sign-in (a valid TraderCode was applied). Use it to confirm the referral landed. See PitPass TraderCodes.
After login(), all REST and WebSocket calls are signed automatically — there is no token to pass.
() => Promise<void>
Revoke the current session on the server, disconnect authenticated WebSocket channels, and clear local auth state.
() => 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.

Backend Authentication

Server-to-server application reporting endpoints do not use a wallet session keypair. Backends send their sk_ secret key in the x-server-key header on every request.
Backend authentication is for server-side application reporting routes only. getApplicationConfig() is session-authenticated and should be called after a user logs in.
Never expose an sk_ secret key in frontend code, mobile apps, browser extensions, logs, or client-side bundles. User trading, deposits, withdrawals, and account routes should use wallet-authorized sessions.

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: The signing string is:

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 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.
(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.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.
(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.
(address, signature, nonce, clientId, session: SessionCredentials) => 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; 404 for an invalid nonce or clientId; and 500 for an internal failure.
() => 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.
() => Promise<void>
Revoke the active session on the server (signed with the session key). sdk.logout() calls this for you.

State Checks

() => boolean
Whether a session is currently active.
() => AuthState | undefined
The current AuthState, or undefined if not authenticated.
(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.
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 hook manages persistence for you.