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
| 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 |
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
- 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.
- 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.
- 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 configuration | Supported | Requirement |
|---|---|---|
| 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 type | Where sufwan-local-test 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, 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 prompt | None. addSigners runs headlessly — the signed-in session is the authorization. To ask for consent, show your own message first. |
| Calling it twice | Privy rejects the repeat with a Duplicate signer error and leaves the wallet unchanged. The example treats it as done. |
| Wallets and funds | Unchanged. sufwan-local-test links the user's existing wallets. |
| Deposit addresses | Can 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:
| 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
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
| 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 the token comes from the same Privy app whose credentials you saved in sufwan-local-test |
409 PRIVY_WALLET_NOT_SIGNABLE from deposit addresses | The user's existing wallet does not have your key quorum as a signer | Add it from the user's session — see Existing wallets without your signer |
Duplicate signer(s) provided when updating wallet | Your signer is already on that wallet | Nothing to do — treat it as success |
| A user has two wallets on one chain | Your onboarding and sufwan-local-test 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 |