GeminiGemini
SandboxGet API key
  • Crypto Trading
  • Prediction Markets
  • SDKs & Tools
Changelog
Gemini Crypto Exchange LogoGemini Crypto Exchange Logo

© 2026 Gemini Space Station, Inc.

Overview
TypeScript SDK
    QuickstartAuthenticationWebSocketError HandlingPatterns & Recipes
    API Reference
    Deep Dives
      Order Book ReconstructionRFQ ProtocolData TypesRequest ValidationWebSocket SessionsTransport & Signing
Deep Dives

Deep Dive — Transport and Signing

The SDK handles request signing, nonce management, retry logic, and dispatch automatically. Understanding these internals helps you debug authentication errors, reason about retry behavior, and configure timeouts correctly.

HMAC signing flow

Every authenticated REST request goes through a signing pipeline that produces three headers. The SDK handles all of this — you never construct these headers yourself.

Step by step

  1. Build the payload object — the SDK combines the API path with your request parameters and a nonce:

    Code
    { "request": "/v1/order/new", "nonce": 1691234567890, "symbol": "BTCUSD", "amount": "0.01", "price": "50000", "side": "buy", "type": "exchange limit" }
  2. Serialize to JSON — JSON.stringify with a bigint-safe replacer (bigints are serialized via JSON.rawJSON to avoid precision loss)

  3. Base64-encode — the JSON string is encoded to base64

  4. HMAC-SHA384 sign — the base64 payload is signed with your API secret using crypto.subtle.sign("HMAC", key, payload)

  5. Attach headers — three headers are sent:

    HeaderValue
    X-GEMINI-APIKEYYour API key
    X-GEMINI-PAYLOADBase64-encoded JSON payload
    X-GEMINI-SIGNATUREHex-encoded HMAC-SHA384 of the payload

The request body is empty (Content-Length: 0). The logical body is encoded entirely within X-GEMINI-PAYLOAD. This is a distinctive aspect of Gemini's private REST API.

Endpoint integrity

The request field in the payload is always set to the API path. If a caller's parameters contain a request key that would override the path, the SDK throws an EndpointMismatch error — preventing a signed payload from being routed to the wrong endpoint.

Nonce serialization and the sign queue

The exchange rejects requests with a nonce that is not strictly greater than the previous one (for monotonic mode). When multiple requests are in-flight concurrently, their nonces must arrive in order at the server.

The problem

crypto.subtle.sign() is asynchronous. If two requests call nextNonce() in sequence (getting nonces N and N+1), then sign concurrently, the signing for N+1 could complete before N. The server sees N+1 first, then rejects N as a stale nonce — even though both were valid when issued.

The solution: sign queue

HmacAuth chains every credentialHeaders() call through a Promise queue (#signQueue):

Code
Request A: nextNonce() → 100 → sign(100) → resolve headers Request B: nextNonce() → 101 → waits for A's sign → sign(101) → resolve headers Request C: nextNonce() → 102 → waits for B's sign → sign(102) → resolve headers

Each signing operation waits for the previous one to complete before starting. This guarantees that credential headers resolve in nonce order, so fetch() calls dispatch in nonce order.

The queue is fire-and-forget for error propagation: if signing fails for request A, request B still proceeds (the queue's .then(() => {}, () => {}) swallows A's rejection). Each request independently handles its own signing errors.

Nonce modes

The SDK supports two nonce strategies:

ModeBehaviorUse case
"monotonic" (default)Millisecond timestamp, bumped by 1 if not strictly increasingGeneral use; safe for high-frequency concurrent requests
"time-based"Unix timestamp in seconds (Math.floor(now / 1000))Required by some account-level endpoints
Code
import { HmacAuth } from "gemini-markets/server"; const auth = new HmacAuth({ apiKey: "your-api-key", apiSecret: "your-api-secret", nonceMode: "monotonic", // default });

Public vs private dispatch

The SDK uses two distinct dispatch paths based on whether an endpoint requires authentication.

Public endpoints (GET, no auth)

Public endpoints like market data use standard HTTP GET with query parameters:

Code
GET /v1/pubticker/BTCUSD HTTP/1.1 Host: api.gemini.com

No X-GEMINI-* headers, no payload signing, no nonce. Parameters are serialized as URL query strings.

Private endpoints (POST, signed payload)

Private endpoints use POST with the signed payload in headers and an empty body:

Code
POST /v1/order/new HTTP/1.1 Host: api.gemini.com Content-Length: 0 Content-Type: text/plain Cache-Control: no-cache X-GEMINI-APIKEY: your-api-key X-GEMINI-PAYLOAD: eyJyZXF1ZXN0Ijoi... X-GEMINI-SIGNATURE: a1b2c3d4...

The Content-Type: text/plain and Content-Length: 0 are deliberate — the Gemini private REST convention encodes the entire request in headers, not the body.

Reserved headers

The SDK prevents both callers and auth strategies from setting headers that would conflict with the transport envelope:

  • X-GEMINI-PAYLOAD, Content-Length, Content-Type, Cache-Control — always set by the transport
  • X-GEMINI-APIKEY, X-GEMINI-SIGNATURE — always set by the auth strategy
  • nonce in request params — reserved for the auth strategy

Attempting to set these throws an SdkError.

OAuth dispatch

OAuth-authenticated requests use a simpler flow: the X-GEMINI-PAYLOAD header is still sent (the exchange always expects it), but there is no HMAC signature and no nonce.

How it works

  1. The OAuthAuth strategy loads tokens from the caller-provided OAuthTokenStore
  2. If the access token is expired (or within the 60-second refresh skew), the token is refreshed automatically
  3. The access token is sent as a Bearer token:
Code
Authorization: Bearer eyJhbGciOi...

Serialized refresh

Token refresh is serialized through the token store's runExclusive method. This prevents concurrent requests from triggering multiple refresh calls (which would fail because refresh tokens are single-use):

Code
Request A: token expired → runExclusive → refresh → save → continue Request B: token expired → runExclusive → load (sees A's fresh token) → continue

Without serialization, both A and B would attempt to refresh with the same single-use refresh token. One would succeed; the other would get invalid_grant and clear the token store.

WebSocket auth

WebSocket connections authenticate differently from REST. The WsSession calls websocketAuthHeaders() which:

  1. Gets a nonce from the auth strategy
  2. Base64-encodes just the nonce (not a full payload envelope)
  3. Gets credential headers (HMAC signature of the encoded nonce)
  4. Sends X-GEMINI-NONCE and X-GEMINI-PAYLOAD as WebSocket upgrade headers

For OAuth-authenticated WebSocket connections, only the Authorization: Bearer ... header is sent (no nonce or payload).

Retry policy

Only operations marked retryable: true in their metadata are retried. In practice, this means all GET-backed reads (public market data, etc.). POST mutations are never retried — replaying a createNewOrder could cause a duplicate fill.

Retried conditions

ConditionBehavior
Network failure (ECONNRESET, ECONNREFUSED, ETIMEDOUT, etc.)Retry with backoff
HTTP 429 (rate limited)Retry, respecting Retry-After header if present
HTTP 502, 503, 504 (server errors)Retry with backoff

Backoff parameters

ParameterDefaultNotes
Base delay500 msConfigurable via backoff.baseMs
Cap30 sConfigurable via backoff.capMs
Factor2Configurable via backoff.factor
Max retries5Configurable via maxRetries
JitterEqual jitterHalf fixed + half random

The backoff formula for attempt N (0-based): min(capMs, baseMs × factor^N), then split into half fixed + half random. This is the same equal-jitter shape used by the WebSocket transport (though with a different base: 500 ms for HTTP vs 250 ms for WebSocket). Note that WebSocket reconnect attempt 0 is immediate (delay = 0) to recover from transient drops quickly; HTTP retries always apply the computed backoff.

Retry-After header

When the server sends a Retry-After header on a 429 response, the SDK respects it:

  • Integer value — interpreted as seconds (e.g. Retry-After: 5 → wait 5 seconds)
  • HTTP date — parsed and used as an absolute deadline
  • Missing or unparseable — falls back to the calculated backoff delay

What is NOT retried

  • POST mutations (order placement, cancellation, withdrawals, transfers)
  • Client errors (400, 403, 404, 406)
  • Auth errors (invalid nonce, invalid signature, missing role)
  • Validation errors (caught before the request is sent)

Request validation pipeline

For operations with client-side validators (e.g. createNewOrder, placeOrder, withdrawCryptoFunds), validation runs before authentication signing. The full pipeline:

Code
validate body → add nonce + request fields → JSON.stringify → base64 → HMAC sign → fetch

This ordering is important: validation catches malformed requests before any signing work. A validation failure never generates a nonce, so the nonce sequence stays clean.

See the Request Validation deep dive for details on what each operation validates.

Timeout and cancellation

Every request gets a deadline controlled by timeoutMs (default: 30 seconds). The SDK uses AbortSignal internally to propagate cancellation through every async step.

How it works

Code
// Default 30-second timeout const ticker = await client.marketData.getTicker({ symbol: "BTCUSD" }); // Custom timeout const ticker = await client.marketData.getTicker({ symbol: "BTCUSD", }, { timeoutMs: 5_000 }); // External cancellation const controller = new AbortController(); const ticker = await client.marketData.getTicker({ symbol: "BTCUSD", }, { signal: controller.signal }); // Cancel from outside controller.abort();

Error types

ErrorCause
RequestTimeoutErrorThe deadline elapsed before a response arrived
RequestAbortedErrorThe caller's AbortSignal was aborted

Both propagate through fetch, WebSocket operations, and auth signing. An aborted request never leaves a dangling nonce.

Timeout scope

The timeout starts inside the transport's send() method — after client-side validation has already run. It covers signing → network round-trip → response parsing, but not the validation step. For retried requests, all attempts share a single deadline — later retries have less remaining time. The timeout is per-operation, not per-attempt.

Related

  • Authentication — configuring HMAC and OAuth credentials
  • Error Handling — error types and classification
  • Request Validation deep dive — client-side validation details
  • Data Types deep dive — bigint serialization in payloads
  • API Specifications — full request/response schemas
Last modified on August 14, 2026
WebSocket Sessions
On this page
  • HMAC signing flow
    • Step by step
    • Endpoint integrity
  • Nonce serialization and the sign queue
    • The problem
    • The solution: sign queue
    • Nonce modes
  • Public vs private dispatch
    • Public endpoints (GET, no auth)
    • Private endpoints (POST, signed payload)
    • Reserved headers
  • OAuth dispatch
    • How it works
    • Serialized refresh
    • WebSocket auth
  • Retry policy
    • Retried conditions
    • Backoff parameters
    • Retry-After header
    • What is NOT retried
  • Request validation pipeline
  • Timeout and cancellation
    • How it works
    • Error types
    • Timeout scope
  • Related
JSON
TypeScript
TypeScript