# TypeScript SDK — Authentication

The SDK supports three authentication strategies. Pick the one that matches your application:

| Strategy | Import | Use when |
| --- | --- | --- |
| **HMAC** | `gemini-markets/server` | Server-side apps with API key + secret |
| **OAuth (confidential)** | `gemini-markets/server` | Server-side apps acting on behalf of users |
| **OAuth (public/PKCE)** | `gemini-markets/browser` | Browser and mobile apps — no client secret |

## HMAC authentication

HMAC is the simplest path for server-to-server integration. Every request is signed with your API secret using HMAC-SHA384.

```ts
import { createClient, HmacAuth } from "gemini-markets/server";

const client = await createClient({
  env: "sandbox",
  auth: new HmacAuth({
    apiKey: process.env.GEMINI_API_KEY,
    apiSecret: process.env.GEMINI_API_SECRET,
  }),
});

const balances = await client.accountServices.getAvailableBalances({});
client.close();
```

The SDK handles payload encoding, nonce generation, and signature computation automatically. You never construct headers manually.

### Nonce modes

Every authenticated request includes a nonce to prevent replay attacks. The SDK supports two modes:

```ts
// Monotonic (default) — strictly increasing counter based on Date.now()
new HmacAuth({ apiKey, apiSecret });

// Time-based — Unix epoch seconds. Required by some sandbox configurations.
new HmacAuth({ apiKey, apiSecret, nonceMode: "time-based" });
```

Use `"time-based"` if the sandbox returns `InvalidNonce` errors with the default mode.

### How HMAC signing works

For reference, this is what the SDK does on every authenticated request:

1. Build a JSON payload with the `request` path and a `nonce`
2. Base64-encode the payload → `X-GEMINI-PAYLOAD` header
3. HMAC-SHA384 sign the base64 string with your secret → `X-GEMINI-SIGNATURE` header
4. Send the API key in `X-GEMINI-APIKEY`

See the [API key authentication docs](/authentication/api-key) for the full protocol specification.

## OAuth — confidential server client

Use `OAuthAuth` when your server acts on behalf of a user who has authorized your application. Confidential clients have a `client_secret`.

```ts
import { createClient, OAuthAuth } from "gemini-markets/server";

const auth = new OAuthAuth({
  client: {
    type: "confidential",
    clientId: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_CLIENT_SECRET,
    redirectUri: "https://yourapp.com/callback",
  },
  env: "sandbox",
  tokenStore: myTokenStore, // you implement this
});
```

### Authorization flow

OAuth requires a multi-step flow: redirect the user, receive a callback, exchange the code for tokens.

```ts
// Step 1: Generate the authorization URL
const { url, transaction } = await auth.beginAuthorization([
  "orders:create",
  "orders:read",
  "balances:read",
]);

// Step 2: Redirect the user to `url`
// They log in at Gemini and authorize your app

// Step 3: Handle the callback
const tokens = await auth.completeAuthorization(callbackUrl, transaction);

// Step 4: Use the authenticated client
const client = await createClient({ env: "sandbox", auth });
const positions = await client.predictions.getPositions({ limit: 10 });
```

### Implementing a token store

The SDK does not persist tokens — you provide a `tokenStore` that handles storage. The store must implement four methods:

```ts
interface OAuthTokenStore {
  load(): Promise<OAuthTokens | undefined>;
  save(tokens: OAuthTokens): Promise<void>;
  clear(): Promise<void>;
  runExclusive<T>(operation: () => Promise<T>): Promise<T>;
}
```

`runExclusive` must serialize all token operations. If you run multiple server instances, this must use a distributed lock (e.g., Redis, database row lock). Single-use refresh tokens will fail if two instances try to refresh concurrently.

A minimal in-memory implementation for development:

```ts
class MemoryTokenStore {
  private tokens?: OAuthTokens;

  async load() { return this.tokens; }
  async save(tokens: OAuthTokens) { this.tokens = tokens; }
  async clear() { this.tokens = undefined; }
  async runExclusive<T>(op: () => Promise<T>) { return op(); }
}
```

### Token refresh

Access tokens expire after 24 hours. The SDK refreshes them automatically when `credentialHeaders()` detects an expired token. Refresh happens inside `runExclusive` to prevent concurrent rotation of single-use refresh tokens.

You can tune refresh timing:

```ts
new OAuthAuth({
  // ...
  refreshSkewMs: 60_000, // refresh 60s before expiry (default)
});
```

### Revocation

Revoke tokens explicitly when a user disconnects your app:

```ts
import { HttpTransport } from "gemini-markets/server";

const transport = new HttpTransport({ env: "sandbox", auth });
await auth.revoke(transport);
// tokens are cleared from the store after successful server-side revocation
```

## Browser OAuth (PKCE)

Browser apps cannot hold a client secret. Use `BrowserOAuthAuth` which enforces public-client PKCE at the type level — you cannot accidentally pass a `clientSecret`.

```ts
// login.ts — the page that starts the OAuth flow
import { createClient, BrowserOAuthAuth } from "gemini-markets/browser";

const auth = new BrowserOAuthAuth({
  client: {
    type: "public",
    clientId: "your-client-id",
    redirectUri: "http://localhost:3000/callback",
  },
  env: "sandbox",
  tokenStore: myBrowserTokenStore,
});

// Step 1: Start authorization (generates PKCE challenge automatically)
const { url, transaction } = await auth.beginAuthorization(["orders:read"]);

// Step 2: Persist the transaction — the redirect will lose in-memory state
sessionStorage.setItem("gemini_oauth_tx", JSON.stringify(transaction));

// Step 3: Redirect the user
window.location.href = url;
```

```ts
// callback.ts — the page Gemini redirects back to
const raw = sessionStorage.getItem("gemini_oauth_tx");
if (!raw) throw new Error("OAuth transaction not found — was the flow started?");
const transaction = JSON.parse(raw);
sessionStorage.removeItem("gemini_oauth_tx");

// Step 4: Complete the exchange with the saved transaction
await auth.completeAuthorization(window.location.href, transaction);

// Step 5: Use the client
const client = createClient({ auth });
const events = await client.predictions.listEvents({ status: ["active"] });
```

The PKCE code challenge and verifier are generated automatically using Web Crypto (`crypto.subtle`). The verifier is sent during code exchange — no secret ever leaves the browser.

### Scopes

Scopes control what the OAuth token can access. Request only what your app needs:

| Scope | Grants |
| --- | --- |
| `orders:create` | Place and cancel orders |
| `orders:read` | View orders and trade history |
| `balances:read` | View account balances |
| `addresses:read` | View approved withdrawal addresses |
| `history:read` | View transaction history |

The full scope list is in the [OAuth documentation](/authentication/oauth#oauth-scopes).

### Browser token persistence

In the browser, persist tokens to `localStorage` or `sessionStorage`:

```ts
const browserTokenStore = {
  async load() {
    const raw = localStorage.getItem("gemini_tokens");
    return raw ? JSON.parse(raw) : undefined;
  },
  async save(tokens) {
    localStorage.setItem("gemini_tokens", JSON.stringify(tokens));
  },
  async clear() {
    localStorage.removeItem("gemini_tokens");
  },
  async runExclusive(op) { return op(); },
};
```

For production apps, consider encrypting tokens at rest and using `sessionStorage` for shorter-lived sessions.

## What's next

- [WebSocket](/tools/typescript-sdk/websocket) — real-time streams with authenticated access
- [Error handling](/tools/typescript-sdk/errors) — OAuth-specific error types and recovery
