Skip to main content
Many apps want to give users self-custodial wallets while ensuring the server must approve every transaction, wallet update, and key export. A 2-of-2 key quorum achieves this: one quorum member is the user, the other is an authorization key controlled by your server. Both must sign every request to Privy’s API. This means that even if a user’s account is compromised, an attacker cannot take unilateral action with the wallet. Equally, your server alone cannot move funds without the user’s consent. At a high-level, you will:
1

Create a server authorization key

Generate a P-256 keypair and register the public key with Privy. Your server holds the private key and uses it to co-sign every request.
2

Create a 2-of-2 key quorum

Register a key quorum that contains the user ID and the server authorization key, with an authorization_threshold of 2.
3

Create a wallet owned by the quorum

Create a wallet whose owner is the key quorum. All subsequent actions on this wallet require both signatures.
4

Execute transactions with both signatures

For each request, collect the user’s JWT, obtain a user signing key, sign with the server authorization key, and send both signatures to the Privy API.

1. Create a server authorization key

Your server needs a P-256 keypair. The private key stays on your server; the public key is registered with Privy so it can verify your server’s signatures. Save the private key securely (e.g. in an environment variable or secrets manager). You will reference it as serverAuthorizationPrivateKey in later steps.

2. Create a 2-of-2 key quorum

Once you have the server authorization key’s public key and the user’s Privy user ID, register a key quorum that requires both to sign.
Key quorums containing both user IDs and authorization keys must be created via the SDK or REST API. The Dashboard only supports pure authorization-key quorums.

3. Create a wallet owned by the quorum

Create a wallet and set its owner_id to the key quorum ID from step 2.
Attach policies to the wallet when creating it to further restrict which transactions are allowed, independent of the co-signing requirement.

4. Execute transactions with both signatures

Every request to the Privy API that acts on this wallet must include signatures from both the user and the server. The flow below applies to transactions, wallet updates, and key export.

How it works

Client                          Your server                      Privy API
  |                                  |                               |
  |-- (1) Build request payload      |                               |
  |-- (2) Sign with user key         |                               |
  |    (useAuthorizationSignature)   |                               |
  |                                  |                               |
  |-- (3) Send payload + user sig -->|                               |
  |                                  |-- (4) Sign with server key    |
  |                                  |-- (5) Send request +          |
  |                                  |       both sigs ------------->|
  |                                  |<-- response ------------------|

Step-by-step

1

Build the request payload on the client

Construct the JSON payload describing the request your app intends to make to the Privy API. This payload includes the target URL, HTTP method, required headers, and request body.
const requestPayload = {
  version: 1,
  url: `https://api.privy.io/v1/wallets/${walletId}/rpc`,
  method: 'POST',
  headers: {
    'privy-app-id': 'insert-your-app-id'
  },
  body: {
    caip2: 'eip155:1',
    method: 'eth_sendTransaction',
    chain_type: 'ethereum',
    params: {
      transaction: {
        to: '0xRecipientAddress',
        value: '0x2386f26fc10000',
        data: '0x'
      }
    }
  }
} as const;
2

Sign the payload with the user's key on the client

Use the useAuthorizationSignature hook to sign the payload with the authenticated user’s signing key. The hook handles key retrieval and signing entirely on the client — the user’s private key never leaves the device.
import {useAuthorizationSignature} from '@privy-io/react-auth';

const {generateAuthorizationSignature} = useAuthorizationSignature();

const {signature: userSignature} = await generateAuthorizationSignature(requestPayload);
3

Send the payload and user signature to your server

Forward both the request payload and the user’s signature to your server. Your server will add its own signature before proxying the request to the Privy API.
await fetch('https://your-server.com/api/wallet-action', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    requestPayload,
    userSignature
  })
});
4

Sign with the server key and send to Privy

On your server, generate the server’s authorization signature over the same payload, then send the request to the Privy API with both signatures as a comma-delimited value in the privy-authorization-signature header.
Privy validates that both signatures are present, valid, and correspond to members of the wallet’s key quorum. If either signature is missing or invalid, the request is rejected.

Learn more