Account

Privy Wallet Provider

Use your existing Privy wallets for authentication and trade execution in sufwan-local-test.

Overview

What this integration does

When Privy is configured as your wallet provider:

  • Authentication — users sign in to sufwan-local-test with their Privy access token.
  • Execution — sufwan-local-test signs trades with the wallets in your Privy app.

Your users keep the same wallet addresses and balances.

Authentication

Privy access tokens

Wallets

Your existing Privy wallets

Chains

Ethereum + Solana

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

Quick start

Prepare wallets

Make sure every trading user has an Ethereum and a Solana wallet that sufwan-local-test can sign with. See Wallet ownership.

Configure sufwan-local-test

Save your Privy credentials under Settings → Wallet Provider and click Validate. See Configure Privy in sufwan-local-test.

Authenticate the user

Exchange the user's Privy access token for an sufwan-local-test session. See Authentication & sessions and Authenticate users.

Deposit addresses

Request them from Funding & withdrawals. That page owns the poll. For a Privy wallet-provider app, evmAddress is the authorized Privy wallet.

Fund and trade

Send USDC to the addresses Funding returns, and place orders as usual.

flowchart TD
  A[Privy SDK in your app] -->|access token| B[sufwan-local-test session]
  B --> C[Deposit addresses on Funding]
  C --> D[Fund and trade]

Prerequisites

Before you start, you need:

  • Your Privy App ID
  • Your Privy App Secret
  • Your Privy Authorization Private Key
  • A Privy Signer ID — only if sufwan-local-test should create missing wallets
  • An Ethereum and a Solana wallet for every trading user
ValueWhere in PrivySecret?
App IDApp settingsNo — it is the aud claim and part of a public JWKS URL
App SecretApp settingsYes
Authorization Private KeyWallet API → Authorization keysYes
Signer ID (key quorum ID)Wallet API → Key quorumsNo

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 sufwan-local-test again.

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 sufwan-local-test. sufwan-local-test verifies it against your Privy app's public keys and returns an sufwan-local-test session. There is no callback URL: Privy never redirects.
  2. Wallet setup. The first time a user's deposit addresses are requested, sufwan-local-test 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

sufwan-local-test can only use a wallet it can sign with on its own:

Wallet configurationSupportedRequirement
User-owned + server signer✅Your key quorum is attached as an additional signer and contains the authorization key saved in sufwan-local-test
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 — sufwan-local-test rejects it with PRIVY_WALLET_NOT_SIGNABLE

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

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

Important — sufwan-local-test 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.

If some existing wallets do not have your server signer yet, the user has to add it from your app — see Existing wallets without your signer. Also check that no Privy policy blocks your authorization key.

Wallet discovery

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

Wallet typeWhere sufwan-local-test finds it
User-ownedThe user's linked accounts in Privy. No entity needed.
Server-ownedentity: { id: "<your Privy user id>", type: "user" }

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

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

Wallet creation

By default sufwan-local-test 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. sufwan-local-test then creates the missing wallet as a user-owned wallet with your signer attached:

{
  "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 sufwan-local-test creates. sufwan-local-test never creates an app-owned wallet, and the setting cannot be saved without a Signer ID.

sufwan-local-test does not replace existing wallets. Creation only fills a chain where the user has no wallet. If sufwan-local-test 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.

Avoid duplicate wallets. If your onboarding already creates wallets — possibly after the user has signed in to sufwan-local-test — leave Create wallets for users who have none disabled. Otherwise both sides can create a wallet for the same chain.

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.

Existing wallets without your signer

Embedded wallets that Privy created at signup are owned by the user. If your key quorum was never added to them, sufwan-local-test cannot sign with them, and GET /execution/deposit-addresses answers 409 with code: "PRIVY_WALLET_NOT_SIGNABLE".

Create wallets for users who have none does not help here — it only fills a chain with no wallet, and never replaces one your user may already have funded.

Only the wallet's owner can change its signers, so your server key cannot add the signer on its own. Add it from your frontend, with the user's Privy session, using Privy's useSigners hook:

import { usePrivy, useSigners } from "@privy-io/react-auth";

// YOUR Privy key quorum ID — the same value you entered as "Signer ID" under
// Settings → Wallet Provider in sufwan-local-test. It is not issued by sufwan-local-test: it comes from
// your Privy dashboard, and the quorum must contain the authorization key
// you saved in sufwan-local-test.
const YOUR_PRIVY_SIGNER_ID = "<your-privy-key-quorum-id>";

export function useAddSignerToWallets() {
  const { user } = usePrivy();
  const { addSigners } = useSigners();

  return async function addSignerToWallets() {
    // Your users' Privy embedded wallets, on both chains — sufwan-local-test signs on each.
    const wallets = (user?.linkedAccounts ?? []).filter(
      (account) =>
        account.type === "wallet" &&
        account.walletClientType === "privy",
    );

    for (const wallet of wallets) {
      try {
        await addSigners({
          address: wallet.address,
          signers: [{ signerId: YOUR_PRIVY_SIGNER_ID }],
        });
      } catch (err) {
        // Privy rejects adding a signer the wallet already has. That means this
        // wallet is already set up — skip it and continue with the next one.
        if (String((err as Error)?.message).includes("Duplicate signer")) continue;
        throw err;
      }
    }
  };
}

Call it after sign-in, before you request deposit addresses, or when that request returns PRIVY_WALLET_NOT_SIGNABLE. Deposit address hooks surface the same code as error. The request itself is on Funding & withdrawals.

What to expect

User promptNone. addSigners runs headlessly — the signed-in session is the authorization. To ask for consent, show your own message first.
Calling it twicePrivy rejects the repeat with a Duplicate signer error and leaves the wallet unchanged. The example treats it as done.
Wallets and fundsUnchanged. sufwan-local-test links the user's existing wallets.
Deposit addressesCan take up to 5 minutes after a 409 — sufwan-local-test rechecks the wallet once its cached refusal expires.

On older versions of Privy's React SDK without useSigners, use useSessionSigners().addSessionSigners — it takes the same arguments.

Users who have no wallet yet do not need this: with Create wallets for users who have none turned on, sufwan-local-test creates their wallets with your signer already attached.

Configure Privy in sufwan-local-test

Open Wallet Provider settings

In the sufwan-local-test dashboard, go to Settings → Wallet Provider.

Choose Privy

Change the provider from Portal (default) to Privy.

Enter credentials

Enter the App ID, App Secret, and Authorization Private Key. To let sufwan-local-test create missing wallets, also turn on Create wallets for users who have none and enter your Signer ID.

Save

Secrets are encrypted before storage and never returned to the dashboard — the form only shows whether they are set.

Validate

Checks your credentials against Privy from the service that signs trades, so a typo fails here instead of during a trade.

Allow your domains

Under Domains, add every origin your frontend calls sufwan-local-test from. Browser requests from other origins are rejected with 403 Origin not allowed for this app. Server-to-server calls are not affected.

Authenticate users

Get the user's access token from Privy's client SDK, then exchange it for an sufwan-local-test session with POST /auth/verify and { "kind": "privy", "privyToken" }. There is no Privy setting on the Auth tab — saving Privy as the wallet provider enables it, and there is no callback URL. See Authentication & sessions.

sufwan-local-test 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.

Signing in does not set up the user's wallets. Deposit addresses are on Funding & withdrawals. For a Privy wallet-provider app, evmAddress is the authorized Privy wallet.

Sign-in errors

Every failure is a 401:

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

Initialize user wallets

For a Privy wallet-provider app, the deposit address is the authorized Privy wallet: evmAddress on EVM and svmAddress on Solana. Other apps get a managed deposit address. Follow the polling example in Funding & withdrawals until provisioning is ready.

A 409 means the Privy wallet does not grant Agg's authorization key signing rights. Add your signer, then request addresses from Funding. See Existing wallets without your signer.

The wallets.ready webhook fires once a user's wallets are set up.

Provider lock

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 sufwan-local-test 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.

Errors & troubleshooting

ProblemLikely causeFix
Validate returns 401The credentials belong to different Privy appsRe-copy all three credentials from the same Privy app
Web3 authentication failed on sign-inThe token was issued by another Privy appCheck the token comes from the same Privy app whose credentials you saved in sufwan-local-test
409 PRIVY_WALLET_NOT_SIGNABLE from deposit addressesThe user's existing wallet does not have your key quorum as a signerAdd it from the user's session — see Existing wallets without your signer
Duplicate signer(s) provided when updating walletYour signer is already on that walletNothing to do — treat it as success
A user has two wallets on one chainYour onboarding and sufwan-local-test both created oneDisable Create wallets for users who have none
403 Origin not allowed for this appYour frontend domain is not allowedAdd it under Domains
Validate passes but trades failA Privy policy or quorum member blocks the authorization keyReview the wallet's policies and key quorum in Privy

On this page