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

> ## Agent Instructions
> To integrate AGG, start with Quickstart: REST (https://docs.agg.market/quickstart/rest), then Order lifecycle & statuses (https://docs.agg.market/concepts/order-lifecycle).
> Track every trade until it reaches a terminal status. Before retrying a failed or timed-out call, read Errors, retries & idempotency (https://docs.agg.market/concepts/errors).
> The API reference is generated from https://docs.agg.market/openapi/openapi.json.

# Authentication & sessions

> How users sign in, what tokens you get back, and how to keep a session alive

AGG authenticates two things: your **app**, with the `x-app-id` header, and a **user**, with an
access token in `Authorization: Bearer <accessToken>`. Market data and quotes without a user need
only the app. Balances, deposits, trading, and withdrawals need a user. See
[Base URL, app IDs & API keys](/environments) for which headers go on which call.

Users sign in on the client, and your backend includes that user's access token on trading calls.

## Sign-in methods

| Provider | How it completes | Start with |
| - | - | - |
| `siwe` (Ethereum wallet) | The wallet signs a message; `POST /auth/verify` returns tokens | `POST /auth/start` returns a `nonce` |
| `siws` (Solana wallet) | Same as SIWE, with a Solana message | `POST /auth/start` returns a `nonce` |
| `google`, `twitter`, `apple` | AGG redirects back to your `redirectUrl` with `?code=`; `POST /auth/token/exchange` returns tokens | `POST /auth/start` returns a redirect `url` |
| `email` | The user opens a magic link that redirects back with `?code=`; exchange it the same way | `POST /auth/start` sends the email |
| `privy` | `POST /auth/verify` with `{ "kind": "privy", "privyToken": "..." }` | No start call |

Code for each provider is in [Sign-in providers](/recipes/authentication).

## Wallet sign-in flow

<Steps>
  <Step title="Get a nonce">
    `POST /auth/start` with `{ "provider": "siwe" }` and `x-app-id`. The response is
    `{ "type": "nonce", "nonce": "...", "statement"?: "..." }`. When bot protection is active it
    can instead be `{ "type": "challenge_required", "siteKey": "..." }`. See
    [Bot protection](/recipes/bot-protection).
  </Step>

  <Step title="Build and sign the message">
    Build an EIP-4361 message with that nonce. Its domain must be one of your app's allowed
    origins. The SDK's `buildSiweMessage` produces the exact format. The user's wallet signs it.
  </Step>

  <Step title="Verify">
    `POST /auth/verify` with `{ "message": "...", "signature": "0x..." }`. The response is
    `{ accessToken, refreshToken, user }`.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agg.market/auth/start \
    -H "x-app-id: $AGG_APP_ID" -H "content-type: application/json" \
    -d '{ "provider": "siwe" }'

  # Sign the EIP-4361 message with the wallet, then:
  curl -X POST https://api.agg.market/auth/verify \
    -H "x-app-id: $AGG_APP_ID" -H "content-type: application/json" \
    -d "$(jq -n --arg m "$MESSAGE" --arg s "$SIGNATURE" '{message: $m, signature: $s}')"
  ```

  ```ts SDK theme={null}
  import { createAggClient } from "@agg-build/sdk";

  const client = createAggClient({ baseUrl: "https://api.agg.market", appId: "your-app-id" });

  const { nonce } = await client.authStart({ provider: "siwe" });
  const message = client.buildSiweMessage({
    address: walletAddress,
    chainId: 1,
    nonce,
    domain: window.location.host,
    uri: window.location.origin,
  });
  const signature = await signMessage({ message }); // wagmi, viem, ethers, ...
  const { accessToken, refreshToken, user } = await client.verify({ message, signature });
  ```
</CodeGroup>

A nonce is single-use and short-lived. Get a new one for each attempt.

## Tokens

| Token | Use | Lifetime |
| - | - | - |
| `accessToken` | `Authorization: Bearer` on user-tier calls | Short-lived. Refresh it when a call returns `401`. |
| `refreshToken` | Body of `POST /auth/token/refresh` | Longer-lived. Replace it with the new one each time you refresh. |

The access token is bound to the app that issued it. Using it with another app's `x-app-id` fails.

The SDK stores the session and adds the header for you. In the browser, set
`authDelivery: "cookie-refresh"` on `createAggClient` to keep the refresh token in an `HttpOnly`
cookie scoped to the AGG API host and `/auth` routes. In that mode `verify()` and
`exchangeAuthCode()` can omit `refreshToken` from the JSON response.

If your backend already holds a user's tokens, seed an SDK client with
`client.setSession({ accessToken, refreshToken })`.

## Refresh a session

When a user-tier call returns `401`, refresh once and retry. If the refresh also fails, sign the
user in again.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agg.market/auth/token/refresh \
    -H "x-app-id: $AGG_APP_ID" -H "content-type: application/json" \
    -d "{ \"refreshToken\": \"$REFRESH_TOKEN\" }"
  # -> { "accessToken": "...", "refreshToken": "...", "user": { ... } }
  ```

  ```ts SDK theme={null}
  async function withAutoRefresh<T>(fn: () => Promise<T>): Promise<T> {
    try {
      return await fn();
    } catch (err: any) {
      if (err.status === 401 && client.isAuthenticated) {
        try {
          await client.refreshAccessToken();
          return await fn();
        } catch {
          await client.signOut();
          throw new Error("Session expired. Please sign in again.");
        }
      }
      throw err;
    }
  }

  const balances = await withAutoRefresh(() => client.getManagedBalances());
  ```
</CodeGroup>

React to sign-out anywhere in your app:

```ts theme={null}
client.onAuthStateChange((authenticated) => {
  if (!authenticated) router.push("/sign-in");
});
```

If you also stream authenticated WebSocket events, refresh the REST session first, then
re-authenticate or reconnect the socket. See [User notifications](/recipes/websocket-notifications).

## Sign out

`POST /auth/sign-out` with the user's token revokes all of that user's tokens. `client.signOut()`
also clears the stored session.

## Sign-in errors

| Status | Meaning |
| - | - |
| `401` with `code: "unregistered_domain"` | The SIWE/SIWS message domain is not an allowed origin. |
| `401` | Bad signature, reused or expired nonce, or an invalid Privy token. |
| `403` | Turnstile token invalid, or the app hit its testing-mode user cap. |
| `429` | Too many sign-in attempts. Wait for `Retry-After`. |

## Related

<CardGroup cols={2}>
  <Card title="Sign-in providers" icon="key" href="/recipes/authentication">
    Code for SIWE, SIWS, Google, X, Apple, email, and Privy.
  </Card>

  <Card title="Account linking" icon="link" href="/recipes/account-linking">
    Add more sign-in methods or wallets to one user.
  </Card>
</CardGroup>
