> ## 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.

# Privy Wallet Provider

> Use your existing Privy wallets for authentication and trade execution in AGG.

## Overview

<Info>
  **What this integration does**

  When Privy is configured as your wallet provider:

  * **Authentication** — users sign in to AGG with their Privy access token.
  * **Execution** — AGG signs trades with the wallets in your Privy app.

  Your users keep the same wallet addresses and balances.
</Info>

<CardGroup cols={3}>
  <Card title="Authentication" icon="key">
    Privy access tokens
  </Card>

  <Card title="Wallets" icon="wallet">
    Your existing Privy wallets
  </Card>

  <Card title="Chains" icon="link">
    Ethereum + Solana
  </Card>
</CardGroup>

Both effects come from one setup in one Privy app. Privy sign-in is not available
on its own, because AGG finds a user's Privy wallets through the Privy identity
that sign-in records.

## Quick start

<Steps>
  <Step title="Prepare wallets">
    Make sure every trading user has an Ethereum and a Solana wallet that AGG can
    sign with. See [Wallet ownership](#wallet-ownership).
  </Step>

  <Step title="Configure AGG">
    Save your Privy credentials under **Settings → Wallet Provider** and click
    **Validate**. See [Configure Privy in AGG](#configure-privy-in-agg).
  </Step>

  <Step title="Authenticate the user">
    Exchange the user's Privy access token for an AGG session with
    `POST /auth/verify`. See [Authenticate users](#authenticate-users).
  </Step>

  <Step title="Initialize wallets">
    Call `GET /execution/deposit-addresses` and poll until `ready: true`. See
    [Initialize user wallets](#initialize-user-wallets).
  </Step>

  <Step title="Fund and trade">
    Send USDC to the returned addresses and place orders as usual.
  </Step>
</Steps>

```mermaid theme={null}
flowchart TD
  A[Privy SDK in your app] -->|access token| B["POST /auth/verify"]
  B -->|AGG session| C["GET /execution/deposit-addresses"]
  C -->|ready: true| D[Ethereum + Solana addresses]
  D --> E[Fund]
  E --> F[Trade]
```

## Prerequisites

<Info>
  **Before you start**, you need:

  * Your Privy **App ID**
  * Your Privy **App Secret**
  * Your Privy **Authorization Private Key**
  * A Privy **Signer ID** — only if AGG should create missing wallets
  * An **Ethereum and a Solana wallet** for every trading user
</Info>

| Value                         | Where in Privy                  | Secret?                                                  |
| ----------------------------- | ------------------------------- | -------------------------------------------------------- |
| **App ID**                    | App settings                    | No — it is the `aud` claim and part of a public JWKS URL |
| **App Secret**                | App settings                    | Yes                                                      |
| **Authorization Private Key** | Wallet API → Authorization keys | Yes                                                      |
| **Signer ID** (key quorum ID) | Wallet API → Key quorums        | No                                                       |

<Warning>
  **Privy shows the authorization private key only once.** Store your own copy
  before you leave the Privy dashboard. If you lose it, create a new
  authorization key in Privy and save it in AGG again.
</Warning>

The key is accepted in any of the shapes Privy hands out: the
`wallet-auth:MIGH...` form, a bare base64 PKCS#8 body, or a full
`-----BEGIN PRIVATE KEY-----` PEM block.

## How it works

1. **Sign-in.** Your frontend gets an access token from Privy and posts it to
   AGG. AGG verifies it against your Privy app's public keys and returns an AGG
   session. There is no callback URL: Privy never redirects.
2. **Wallet setup.** The first time a user's deposit addresses are requested,
   AGG finds that user's Ethereum and Solana wallets in your Privy app and links
   them. If you opted in, it creates any that are missing.
3. **Execution.** Every trade, bridge and transfer is signed through Privy with
   the authorization key you saved, with no user present.

## Wallet ownership

AGG can only use a wallet it can sign with on its own:

| Wallet configuration             | Supported | Requirement                                                                                         |
| -------------------------------- | :-------: | --------------------------------------------------------------------------------------------------- |
| User-owned + server signer       |     ✅     | Your key quorum is attached as an additional signer and contains the authorization key saved in AGG |
| Server-owned                     |     ✅     | The owner is your authorization key, or a key quorum containing it                                  |
| User-owned without server signer |     ❌     | Would need the user's own session to sign — AGG rejects it with `PRIVY_WALLET_NOT_SIGNABLE`         |

A user-owned wallet with your server signer looks like this in Privy:

```json theme={null}
{
  "owner": { "user_id": "did:privy:…" },
  "additional_signers": [{ "signer_id": "<your key quorum id>" }]
}
```

<Warning>
  **Important — AGG must sign without the user being present.** Limit-order
  fills, settlement webhooks, and managed balance refills run after the
  original user request has ended, some of them minutes or hours later.
</Warning>

<Tip>
  If some existing wallets do not have your server signer yet, attach it in
  Privy. Those users can trade from the next attempt — nothing changes on the
  AGG side. Also check that no Privy policy blocks your authorization key.
</Tip>

## Wallet discovery

AGG looks for a user's wallets in two places, so either ownership model works
without extra configuration:

| Wallet type  | Where AGG finds it                                       |
| ------------ | -------------------------------------------------------- |
| User-owned   | The user's linked accounts in Privy. No `entity` needed. |
| Server-owned | `entity: { id: "<your Privy user id>", type: "user" }`   |

Of the wallets found, AGG uses the first one per chain that it can actually sign
with, proven by signing a harmless message.

<Note>
  Entity is **immutable once set**. A server-owned wallet created without one can
  never be attributed to a user.
</Note>

## Wallet creation

By default AGG **never creates a wallet** — it uses the ones your onboarding
already made.

If your users can reach trading before a wallet exists, turn on **Create wallets
for users who have none** and enter your **Signer ID**. AGG then creates the
missing wallet as a user-owned wallet with your signer attached:

```json theme={null}
{
  "chain_type": "ethereum",
  "owner": { "user_id": "did:privy:…" },
  "additional_signers": [{ "signer_id": "<your signer id>" }]
}
```

Use the key quorum your backend already attaches to your users' wallets, so your
signer policies and key rotation also cover wallets AGG creates. AGG never
creates an app-owned wallet, and the setting cannot be saved without a Signer ID.

<Note>
  **AGG does not replace existing wallets.** Creation only fills a chain where
  the user has no wallet. If AGG cannot sign with an existing wallet, it reports
  that instead of creating a second one, because your user's funds may be in the
  wallet it skipped.
</Note>

<Warning>
  **Avoid duplicate wallets.** If your onboarding already creates wallets —
  possibly after the user has signed in to AGG — leave **Create wallets for
  users who have none** disabled. Otherwise both sides can create a wallet for
  the same chain.
</Warning>

Unlike the wallet provider itself, this setting can be changed at any time. A
change applies the next time a user's wallets are set up.

## Configure Privy in AGG

<Steps>
  <Step title="Open Wallet Provider settings">
    In the AGG dashboard, go to **Settings → Wallet Provider**.
  </Step>

  <Step title="Choose Privy">
    Change the provider from **Portal (default)** to **Privy**.
  </Step>

  <Step title="Enter credentials">
    Enter the App ID, App Secret, and Authorization Private Key. To let AGG
    create missing wallets, also turn on **Create wallets for users who have
    none** and enter your Signer ID.
  </Step>

  <Step title="Save">
    Secrets are encrypted before storage and never returned to the dashboard —
    the form only shows whether they are set.
  </Step>

  <Step title="Validate">
    Checks your credentials against Privy from the service that signs trades, so
    a typo fails here instead of during a trade.
  </Step>

  <Step title="Allow your domains">
    Under **Domains**, add every origin your frontend calls AGG from. Browser
    requests from other origins are rejected with `403 Origin not allowed for
            this app`. Server-to-server calls are not affected.
  </Step>
</Steps>

<Tip>
  If **Require API key** is turned on for your app, send your key in the
  `x-app-api-key` header on every request below.
</Tip>

## Authenticate users

Get the user's access token from Privy's client SDK, then exchange it for an AGG
session. There is no Privy setting on the **Auth** tab — saving Privy as the
wallet provider enables it.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.agg.market/auth/verify \
    -H "Content-Type: application/json" \
    -H "x-app-id: YOUR_APP_ID" \
    -d '{
      "kind": "privy",
      "privyToken": "PRIVY_ACCESS_TOKEN"
    }'
  ```

  ```ts JavaScript theme={null}
  await client.verify({
    kind: "privy",
    privyToken,
  });
  ```
</CodeGroup>

This is the same `/auth/verify` endpoint used for wallet sign-in; the body is a
union on `kind`. A success returns the same access and refresh tokens as every
other AGG sign-in method.

AGG verifies the token with Privy's published public keys at
`https://auth.privy.io/api/v1/apps/{appId}/jwks.json`. The Privy DID (`sub`)
becomes the user's identity, so the same person signing in to two of your apps
gets one identity across both.

<Note>
  Signing in does **not** set up the user's wallets. Do that next.
</Note>

### Sign-in errors

Every failure is a `401`:

| Message                                     | Meaning                                                                                                       |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `Privy sign-in is not enabled for this app` | The app's wallet provider is not Privy, or no Privy credentials are saved.                                    |
| `Web3 authentication failed`                | The token is expired, malformed, or issued by a different Privy app than the one saved under Wallet Provider. |

## Initialize user wallets

Request the user's deposit addresses right after sign-in. The first request
starts wallet setup.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.agg.market/execution/deposit-addresses \
    -H "x-app-id: YOUR_APP_ID" \
    -H "Authorization: Bearer AGG_ACCESS_TOKEN"
  ```

  ```ts JavaScript theme={null}
  const addresses = await client.getDepositAddresses();
  ```

  ```tsx React theme={null}
  const { isReady, evmAddress, svmAddress } = useDepositAddresses();
  ```
</CodeGroup>

Poll every few seconds until setup finishes. `useDepositAddresses()` polls for
you.

<CodeGroup>
  ```json Initial response theme={null}
  {
    "ready": false
  }
  ```

  ```json Ready theme={null}
  {
    "ready": true,
    "evmAddress": "0x…",
    "svmAddress": "…"
  }
  ```
</CodeGroup>

The returned addresses are the user's Privy wallets. Fund them with USDC — on an
EVM chain to `evmAddress`, or on Solana to `svmAddress` — and place orders as
usual. AGG bridges between chains when needed.

<Tip>
  Instead of polling, subscribe to the
  [`wallets.ready`](/recipes/webhooks/event-reference) webhook, which fires once
  a user's wallets are set up. The deposit, withdraw, and balances components in
  `@agg-build/ui` request deposit addresses automatically when they open.
</Tip>

## Provider lock

<Warning>
  **The wallet provider cannot be migrated.** Once any wallet exists for one of
  your app's users, the provider is permanently locked. To change providers,
  create a new AGG app.

  The two custodians hold key material in different systems and neither exports
  it, so switching would strand every existing wallet and the funds in it. The
  rule is enforced by the API, not just the dashboard.
</Warning>

## Errors & troubleshooting

| Problem                                 | Likely cause                                                 | Fix                                                                          |
| --------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| **Validate** returns 401                | The credentials belong to different Privy apps               | Re-copy all three credentials from the same Privy app                        |
| `Web3 authentication failed` on sign-in | The token was issued by another Privy app                    | Check you are not sending a staging token to a production app, or vice versa |
| A user has two wallets on one chain     | Your onboarding and AGG both created one                     | Disable **Create wallets for users who have none**                           |
| `403 Origin not allowed for this app`   | Your frontend domain is not allowed                          | Add it under **Domains**                                                     |
| Validate passes but trades fail         | A Privy policy or quorum member blocks the authorization key | Review the wallet's policies and key quorum in Privy                         |
