← All Docs

Privy Adapter - Server-Side Wallet Integration

Privy Adapter

Use PolicyLayer with Privy, the simple embedded wallet infrastructure for server-side transaction signing.

Overview

Best for: Server-side AI agents, simple embedded wallets, backend automation Chains: All EVM-compatible blockchains Custody model: Server-custodied embedded wallets (Privy manages keys) Bundle size: ~100kB Setup complexity: Easy

Installation

npm install @policylayer/sdk @privy-io/server-auth

Peer dependencies:

  • @privy-io/server-auth v1.x

Basic Setup

1. Get Privy API credentials

  1. Sign up at https://dashboard.privy.io/
  2. Create a new application
  3. Copy your App ID and App Secret from Settings
  4. Configure authentication methods (email, social, etc.)

2. Import the adapter

import { PolicyWallet, createPrivyAdapter } from '@policylayer/sdk';
import { PrivyClient } from '@privy-io/server-auth';

3. Create the adapter

// Initialise Privy client
const privy = new PrivyClient(
  process.env.PRIVY_APP_ID!,
  process.env.PRIVY_APP_SECRET!
);

// Create adapter for a user's embedded wallet
const adapter = await createPrivyAdapter(
  privy,
  userId, // Privy user ID (e.g., 'did:privy:abc123')
  'base'  // Network: base, ethereum, polygon, etc.
);

4. Wrap with PolicyWallet

const wallet = new PolicyWallet(adapter, {
  apiKey: process.env.POLICYLAYER_API_KEY,
});

5. Send transactions

// Native ETH transfer
await wallet.send({
  chain: 'base',
  asset: 'eth',
  to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
  amount: '1000000000000000000', // 1 ETH in wei
});

// ERC-20 token transfer (e.g., USDC)
await wallet.send({
  chain: 'base',
  asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
  to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
  amount: '1000000', // 1 USDC (6 decimals)
});

Configuration

Environment Variables

# Required - Privy credentials
PRIVY_APP_ID=... # Your Privy App ID
PRIVY_APP_SECRET=... # Your Privy App Secret (NEVER expose client-side!)

# Required - PolicyLayer API key
POLICYLAYER_API_KEY=sk_...

# Optional
PRIVY_NETWORK=base # Default network

Privy Dashboard Configuration

Configure in https://dashboard.privy.io/:

  1. Authentication:

    • Enable email login (magic link)
    • Enable social logins (Google, Twitter, Discord, etc.)
    • Configure session duration
  2. Embedded Wallets:

    • Enable embedded wallet creation
    • Choose networks to support
    • Configure wallet security settings
  3. Security:

    • Add allowed domains (for frontend integration)
    • Configure API access controls
    • Set webhook endpoints (optional)

Complete Example

Server-Side AI Agent

import { PolicyWallet, createPrivyAdapter } from '@policylayer/sdk';
import { PrivyClient } from '@privy-io/server-auth';
import * as dotenv from 'dotenv';

dotenv.config();

async function processRefund(userId: string, amount: string, recipient: string) {
  // 1. Initialise Privy client
  const privy = new PrivyClient(
    process.env.PRIVY_APP_ID!,
    process.env.PRIVY_APP_SECRET!
  );

  // 2. Create Privy adapter for user's wallet
  const adapter = await createPrivyAdapter(privy, userId, 'base');

  // 3. Create PolicyWallet
  const wallet = new PolicyWallet(adapter, {
    apiKey: process.env.POLICYLAYER_API_KEY!,
  });

  try {
    // 4. Send USDC refund with policy enforcement
    const result = await wallet.send({
      chain: 'base',
      asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
      to: recipient,
      amount: amount, // Amount in USDC (6 decimals)
    });

    console.log('✅ Refund sent:', result.hash);
    console.log('⛽ Gas fee:', result.fee);

    return { success: true, hash: result.hash };
  } catch (error) {
    if (error.code === 'POLICY_DECISION_DENY') {
      console.log('❌ Policy denied:', error.message);
      return {
        success: false,
        code: error.code,
        message: error.message,
        remainingDaily: error.details?.counters?.remainingDaily,
      };
    } else {
      throw error;
    }
  }
}

// Example usage
const result = await processRefund(
  'did:privy:abc123',    // User ID
  '5000000',             // 5 USDC
  '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1'
);

Backend API Endpoint

import { PolicyWallet, createPrivyAdapter } from '@policylayer/sdk';
import { PrivyClient } from '@privy-io/server-auth';
import express from 'express';

const app = express();
app.use(express.json());

const privy = new PrivyClient(
  process.env.PRIVY_APP_ID!,
  process.env.PRIVY_APP_SECRET!
);

app.post('/api/send-payment', async (req, res) => {
  try {
    // 1. Verify Privy auth token from frontend
    const authToken = req.headers.authorization?.replace('Bearer ', '');
    const claims = await privy.verifyAuthToken(authToken!);
    const userId = claims.userId; // e.g., 'did:privy:abc123'

    // 2. Create adapter for user's wallet
    const adapter = await createPrivyAdapter(privy, userId, 'base');

    // 3. Create PolicyWallet
    const wallet = new PolicyWallet(adapter, {
      apiKey: process.env.POLICYLAYER_API_KEY!,
    });

    // 4. Send transaction with policy enforcement
    const result = await wallet.send({
      chain: 'base',
      asset: req.body.token,
      to: req.body.to,
      amount: req.body.amount,
    });

    res.json({
      success: true,
      hash: result.hash,
      fee: result.fee,
    });
  } catch (error) {
    if (error.code === 'POLICY_DECISION_DENY') {
      res.status(403).json({
        error: 'Policy denied',
        code: error.code,
        message: error.message,
        remainingDaily: error.details?.counters?.remainingDaily,
      });
    } else if (error.message.includes('Invalid token')) {
      res.status(401).json({ error: 'Unauthorised' });
    } else {
      res.status(500).json({ error: error.message });
    }
  }
});

app.listen(3000);

Advanced Usage

Get User’s Wallet Address

// Create adapter
const adapter = await createPrivyAdapter(privy, userId, 'base');

// Get wallet address
const address = await adapter.getAddress();
console.log('User wallet address:', address);

Multi-Chain Support

// Base wallet
const baseAdapter = await createPrivyAdapter(privy, userId, 'base');
const baseWallet = new PolicyWallet(baseAdapter, { apiKey: 'key-1' });

// Ethereum wallet
const ethAdapter = await createPrivyAdapter(privy, userId, 'ethereum');
const ethWallet = new PolicyWallet(ethAdapter, { apiKey: 'key-2' });

// Send on different chains
await baseWallet.send({ chain: 'base', asset: 'usdc', to: '0x...', amount: '1000000' });
await ethWallet.send({ chain: 'ethereum', asset: 'eth', to: '0x...', amount: '1000000000000000000' });

Error Handling

import { PolicyError } from '@policylayer/sdk';

try {
  await wallet.send({ chain: 'base', asset: USDC, to: merchant, amount: '1000000' });
} catch (error) {
  if (error instanceof PolicyError) {
    // Policy denied transaction
    console.log('Policy blocked transaction:');
    console.log('- Code:', error.code);  // e.g., 'POLICY_DECISION_DENY'
    console.log('- Message:', error.message);
    console.log('- Remaining daily:', error.details?.counters?.remainingDaily);
  } else if (error.message.includes('User not found')) {
    // User doesn't exist
    console.log('User not found - create Privy account first');
  } else if (error.message.includes('Wallet not created')) {
    // User hasn't created embedded wallet
    console.log('User needs to create embedded wallet');
  } else if (error.message.includes('insufficient funds')) {
    // Balance error
    console.log('Insufficient balance');
  } else {
    // Other error
    console.error('Transaction failed:', error.message);
  }
}

Verify Authentication Token

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

const privy = new PrivyClient(appId, appSecret);

// Verify token from frontend
try {
  const claims = await privy.verifyAuthToken(authToken);
  console.log('User ID:', claims.userId);
  console.log('App ID:', claims.appId);
  console.log('Issued at:', claims.issuedAt);
  console.log('Expiration:', claims.expiration);
} catch (error) {
  console.error('Invalid asset:', error.message);
}

Get User Information

// Get user by ID
const user = await privy.getUser(userId);

console.log('User ID:', user.id);
console.log('Created at:', user.createdAt);
console.log('Linked accounts:', user.linkedAccounts); // email, wallet, social, etc.
console.log('Wallet address:', user.wallet?.address);

Balance Checks

// Get wallet balance
const balance = await adapter.getBalance();
console.log('Wallet balance:', balance, 'wei');

// Convert to ETH
const balanceEth = Number(balance) / 1e18;
console.log('Balance:', balanceEth, 'ETH');

Network Configuration

Supported Networks

Privy supports all EVM chains:

// Ethereum
const adapter = await createPrivyAdapter(privy, userId, 'ethereum');

// Base (recommended for low fees)
const adapter = await createPrivyAdapter(privy, userId, 'base');

// Polygon
const adapter = await createPrivyAdapter(privy, userId, 'polygon');

// Arbitrum
const adapter = await createPrivyAdapter(privy, userId, 'arbitrum');

// Optimism
const adapter = await createPrivyAdapter(privy, userId, 'optimism');

// Avalanche
const adapter = await createPrivyAdapter(privy, userId, 'avalanche');

Network Selection

Configure enabled networks in Privy dashboard:

  1. Go to https://dashboard.privy.io/
  2. Navigate to Settings > Embedded Wallets
  3. Enable desired networks
  4. Save changes

Troubleshooting

”Invalid App ID or App Secret” error

Problem: Privy credentials are incorrect

Solution: Verify credentials from Privy dashboard:

// ✅ Correct
PRIVY_APP_ID=clxyz123...
PRIVY_APP_SECRET=abc123def456...

// ❌ Wrong - swapped or incomplete
PRIVY_APP_ID=abc123...
PRIVY_APP_SECRET=clxyz123...

“User not found” error

Problem: User ID doesn’t exist in Privy

Solution: Ensure user has been created in Privy:

// Users are created when they authenticate via Privy frontend
// Or you can create users programmatically:
const user = await privy.createUser({
  email: 'user@example.com',
  // ... other fields
});

console.log('User ID:', user.id);

“Wallet not created” error

Problem: User hasn’t created embedded wallet yet

Solution: Users need to create wallet through Privy flow:

// This happens automatically when user:
// 1. Authenticates via Privy (email/social)
// 2. Completes embedded wallet setup

// Check if user has wallet
const user = await privy.getUser(userId);
if (!user.wallet) {
  console.log('User needs to complete wallet setup');
}

“Invalid token” error

Problem: Auth token is expired or malformed

Solution: Get fresh token from frontend:

// Frontend - Ensure token is valid
import { usePrivy } from '@privy-io/react-auth';

const { getAccessToken } = usePrivy();
const token = await getAccessToken();

// Send to backend with Authorization header

“Network not supported” error

Problem: Network not enabled in Privy dashboard

Solution: Enable network in Privy dashboard:

  1. Go to Settings > Embedded Wallets
  2. Enable the network
  3. Save changes

Best Practises

1. Never expose App Secret client-side

// ✅ Good - App Secret only on server
const privy = new PrivyClient(
  process.env.PRIVY_APP_ID!,
  process.env.PRIVY_APP_SECRET! // Server-side only
);

// ❌ NEVER expose App Secret in frontend

2. Always verify auth tokens

// Verify token before processing any transaction
try {
  const claims = await privy.verifyAuthToken(authToken);
  const userId = claims.userId;
  // Proceed with transaction
} catch {
  return res.status(401).json({ error: 'Unauthorised' });
}

3. Handle wallet creation gracefully

async function ensureWallet(privy: PrivyClient, userId: string) {
  const user = await privy.getUser(userId);

  if (!user.wallet) {
    throw new Error('User needs to create embedded wallet first');
  }

  return user.wallet.address;
}

4. Use testnet for development

// Development: Use testnet
const adapter = await createPrivyAdapter(privy, userId, 'base-sepolia');

// Production: Use mainnet
const adapter = await createPrivyAdapter(privy, userId, 'base');

5. Log transactions for monitoring

const result = await wallet.send({ to, amount, token });

// Log for monitoring and debugging
console.log({
  userId,
  txHash: result.hash,
  fee: result.fee,
  timestamp: new Date().toISOString(),
});

// Store in database for audit trail
await db.transactions.create({
  userId,
  txHash: result.hash,
  amount,
  to,
  createdAt: new Date(),
});

Why Choose Privy?

Simple Server-Side Integration

// Just 3 steps:
const privy = new PrivyClient(appId, appSecret);
const adapter = await createPrivyAdapter(privy, userId, 'base');
const wallet = new PolicyWallet(adapter, { apiKey });

// Start sending transactions
await wallet.send({ to, amount });

Embedded Wallets

  • No seed phrases: Users don’t manage keys
  • Email/social login: Simple authentication
  • Automatic creation: Wallet created on first login
  • Recovery: Built-in recovery via email/social

Perfect for AI Agents

// AI agent processes refunds automatically
async function aiRefundAgent(userId: string, reason: string) {
  // AI determines refund amount
  const refundAmount = await ai.calculateRefund(reason);

  // Send refund with policy enforcement
  const adapter = await createPrivyAdapter(privy, userId, 'base');
  const wallet = new PolicyWallet(adapter, { apiKey });

  await wallet.send({
    to: userAddress,
    amount: refundAmount,
    asset: USDC,
  });
}

Lightweight

  • Bundle size: ~100kB (smaller than Dynamic, Coinbase)
  • Simple API: Minimal learning curve
  • Fast setup: Get started in minutes

Comparison with Other Adapters

FeaturePrivyDynamic.xyzethers.js
Server-side focus⚠️
Embedded wallets
Social login
Smart wallets
Setup complexityEasyMediumEasy
Bundle size~100kB~250kB~300kB
Best forBackend agentsConsumer appsFull custody

Use Privy if:

  • You’re building server-side AI agents
  • You want simple embedded wallets
  • You prefer lightweight SDK
  • You don’t need advanced smart wallet features

Use Dynamic.xyz if:

  • You need smart wallet features (ERC-4337)
  • You want advanced frontend components
  • You need more customisation options

Use ethers.js if:

  • You need full self-custody
  • You’re building for technical users
  • Embedded wallets aren’t required

Example Use Cases

Customer Support Refund Agent

// AI agent processes refund requests
async function processRefundRequest(ticketId: string) {
  const ticket = await db.tickets.findById(ticketId);

  // AI determines if refund is warranted
  const shouldRefund = await ai.analyzeTicket(ticket);

  if (shouldRefund) {
    const adapter = await createPrivyAdapter(privy, ticket.userId, 'base');
    const wallet = new PolicyWallet(adapter, { apiKey });

    await wallet.send({
      to: ticket.userWallet,
      amount: ticket.orderAmount,
      asset: USDC,
    });

    await ticket.update({ status: 'refunded' });
  }
}

DeFi Trading Bot

// Trading bot executes swaps for users
async function executeSwap(userId: string, fromToken: string, toToken: string, amount: string) {
  const adapter = await createPrivyAdapter(privy, userId, 'base');
  const wallet = new PolicyWallet(adapter, { apiKey });

  // Policy enforces daily trading limits
  await wallet.send({
    to: DEX_ROUTER_ADDRESS,
    amount: amount,
    asset: fromToken,
    // Swap logic handled by DEX
  });
}

Subscription Payment Agent

// Auto-pay subscriptions
async function processSubscription(userId: string, merchantAddress: string, amount: string) {
  const adapter = await createPrivyAdapter(privy, userId, 'base');
  const wallet = new PolicyWallet(adapter, { apiKey });

  // Policy prevents overspending
  await wallet.send({
    to: merchantAddress,
    amount: amount,
    asset: USDC,
  });

  await db.subscriptions.update({ userId, lastPayment: new Date() });
}

Next Steps

API Reference

createPrivyAdapter()

function createPrivyAdapter(
  privyClient: PrivyClient,
  userId: string,
  network: string
): Promise<WalletAdapter>

Parameters:

  • privyClient - Privy client instance (from @privy-io/server-auth)
  • userId - Privy user ID (e.g., ‘did:privy:abc123’)
  • network - Network identifier (‘base’, ‘ethereum’, ‘polygon’, etc.)

Returns: Promise resolving to WalletAdapter

Example:

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

const privy = new PrivyClient(
  process.env.PRIVY_APP_ID!,
  process.env.PRIVY_APP_SECRET!
);

const adapter = await createPrivyAdapter(
  privy,
  'did:privy:abc123',
  'base'
);

PrivyClient Methods

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

const privy = new PrivyClient(appId, appSecret);

// Verify auth token
const claims = await privy.verifyAuthToken(token);

// Get user by ID
const user = await privy.getUser(userId);

// Create user
const newUser = await privy.createUser({ email: '...' });

// Update user
await privy.updateUser(userId, { ... });

// Delete user
await privy.deleteUser(userId);