Once you have created a wallet with a user signer, you can transact on that wallet with a valid user JWT. To do so, your application will

  1. Generate a SPKI-formatted ECDH P-256 keypair.
  2. Request a time-bound session key from the /v1/user_signers/authenticate endpoint using the user’s JWT and the public key of the ECDH keypair.
  3. Send a transaction to the Wallet API, signed with the time-bound session key.

Generate an ECDH P-256 keypair

The first step to transact with a wallet via a user signer is to generate a SPKI-formatted ECDH P-256 keypair. The public key will be used to encrypt the session key and the private key will be used to decrypt the encapsulated key.

import * as crypto from 'crypto';

async function generateEcdhP256KeyPair(): Promise<{
  privateKey: CryptoKey;
  recipientPublicKey: string;
}> {
  // Generate a P-256 key pair
  const keyPair = await crypto.subtle.generateKey(
    {
      name: 'ECDH',
      namedCurve: 'P-256'
    },
    true,
    ['deriveBits']
  );

  // The privateKey will be used later to decrypt the encapsulatedKey data returned from the /v1/user_signers/authenticate endpoint.
  const privateKey = keyPair.privateKey;

  // The publicKey will be used to encrypt the session key and will be sent to the /v1/user_signers/authenticate endpoint.
  // The publicKey must be a base64-encoded, SPKI-format string
  const publicKeyInSpkiFormat = await crypto.subtle.exportKey('spki', keyPair.publicKey);
  const recipientPublicKey = Buffer.from(publicKeyInSpkiFormat).toString('base64');

  return {privateKey, recipientPublicKey};
}

POST /v1/user_signers/authenticate

Next, request a time-bound session key via the /v1/user_signers/authenticate endpoint. This key will be used to sign the request before it is submitted to the Privy Wallet API. The expiration time of the key is returned in the response.

The /v1/user_signers/authenticate endpoint integrates directly with the JWT-based authentication settings configured in the Privy dashboard. In particular, the JWT is verified according to the registered JWKS.json endpoint. This endpoint uniquely identifies users based on the subject ID (the sub claim) within the JWT and verifies the JWT is authorized to transact on the wallet.

https://api.privy.io/v1/user_signers/authenticate

Body

A request body to /v1/user_signers/authenticate contains the following parameters.

user_jwt
string
required

The user’s JWT, to be used to authenticate the user.

encryption_type
'HPKE
required

The encryption type for the authentication response. Currently only supports HPKE.

recipient_public_key
string
required

The public key of your ECDH keypair, in base64-encoded, SPKI-format, whose private key will be able to decrypt the session key. This keypair must be generated securely and the private key must be kept confidential. The public key sent should be in DER or PEM format. It is recommended to use DER format.

Response

A successful response will contain the following fields.

encrypted_authorization_key
object

The encrypted authorization key, once decrypted, can be used to sign transactions on the wallet, acting as a temporary AuthorizationPrivateKey. Once decrypted, you will need to generate an authorization signature and pass it as a header under privy-authorization-signature.

expires_at
number

The expiration time of the authorization key in seconds since the epoch.

wallets
object[]

The wallets that the signer has access to.

Example

For example, your application may make a request to the /v1/user_signers/authenticate endpoint with the following parameters.

async function authenticateUserWallet() {
  const jwt = 'your-user-jwt';
  const appId = 'your-app-id';
  const appSecret = 'your-app-secret';

  // SPKI-formatted ECDH P-256 public key
  const {recipientPublicKey} = await generateEcdhP256KeyPair();

  const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString('base64');
  const response = await fetch(`https://api.privy.io/v1/user_signers/authenticate`, {
    method: 'POST',
    headers: {
      Authorization: `Basic ${basicAuth}`,
      'Content-Type': 'application/json',
      'privy-app-id': appId
    },
    body: JSON.stringify({
      user_jwt: jwt,
      encryption_type: 'HPKE',
      recipient_public_key: recipientPublicKey
    })
  });

  return await response.json();
}

A successful response will look like the following.

{
  "encrypted_authorization_key": {
    "encryption_type": "HPKE",
    "encapsulated_key": "<encapsulated-key>",
    "ciphertext": "<ciphertext>"
  },
  "expires_at": 1715270400,
  "wallets": [
    {
      "id": "<wallet-id>",
      "chain_type": "ethereum",
      "address": "<wallet-address>"
    }
  ]
}

Decrypt /v1/user_signers/authenticate response

In order to send a transaction, decrypt the encapsulatedKey with the privateKey of your ECDH keypair.

import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256, Chacha20Poly1305} from '@hpke/core';

const resp = await authenticateUserWallet();

const {encrypted_authorization_key} = resp;
const privateKey: CryptoKey = 'private-key-from-generateEcdhP256KeyPair';

async function decryptEncapsulatedKey(encrypted_authorization_key, privateKey): Promise<string> {
  const {encapsulated_key: encapsulatedKey, ciphertext} = encrypted_authorization_key;

  // Step 1: Create the HPKE cipher suite
  const suite = new CipherSuite({
    kem: new DhkemP256HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Chacha20Poly1305()
  });

  // Step 2: Initialize the recipient context using the privateKey and the encapsulated_key
  const context = await suite.createRecipientContext({
    recipientKey: privateKey,
    enc: Buffer.from(encapsulatedKey, 'base64')
  });

  // Step 3: Decrypt the ciphertext (which contains a base64-encoded PKCS8 private key)
  const decrypted = await context.open(Buffer.from(ciphertext, 'base64'));

  // Step 4: Return the decrypted key as a base64-encoded PKCS8 string
  return Buffer.from(decrypted).toString('utf8');
}

This decryptedKey can be used as the authorizationPrivateKey to sign requests to the Wallet API.

Send a transaction via the Wallet API

With the decrypted authorizationPrivateKey, your application can request a transaction via the Wallet API.

Learn how to generate an authorization signature and send a transaction via the Wallet API.

The fastest way to start sending transactions with an authorization signature is via the Server SDK.

import {PrivyClient} from '@privy-io/server-auth';

const resp = await authenticateUserWallet();
const {encrypted_authorization_key, wallets} = resp;
const decryptedKey = await decryptEncapsulatedKey(encrypted_authorization_key, privateKey);
const walletId = wallets[0].id;

const client = new PrivyClient($PRIVY_APP_ID, $PRIVY_APP_SECRET, {
  walletApi: {
    // PrivyClient will automatically inject an authorization signature into the privy-authorization-signature header using this key.
    authorizationPrivateKey: decryptedKey
  }
});

// This request will automatically include the privy-authorization-signature header.
const res = await client.walletApi.ethereum.signMessage({
  walletId: walletId,
  message: 'Hello world'
});