Skip to main content

Privy Adapter

Use SpendSafe 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 @spendsafe/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 '@spendsafe/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.SPENDSAFE_API_KEY,
});

5. Send transactions

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

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

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 - SpendSafe API key
SPENDSAFE_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 '@spendsafe/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.SPENDSAFE_API_KEY!,
});

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

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

return { success: true, hash: result.hash };
} catch (error) {
if (error.code === 'POLICY_VIOLATION') {
console.log('❌ Policy violation:', error.message);
return {
success: false,
reason: error.details.reason,
remainingDaily: error.details.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 '@spendsafe/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.SPENDSAFE_API_KEY!,
});

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

res.json({
success: true,
hash: result.hash,
fee: result.fee,
});
} catch (error) {
if (error.code === 'POLICY_VIOLATION') {
res.status(403).json({
error: 'Policy violation',
reason: error.details.reason,
remainingDaily: error.details.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({ to: '0x...', amount: '1000000' });
await ethWallet.send({ to: '0x...', amount: '1000000000000000000' });

Error Handling

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

try {
await wallet.send({ to: merchant, amount: '1000000', asset: USDC });
} catch (error) {
if (error instanceof PolicyError) {
// Policy violation
console.log('Policy blocked transaction:');
console.log('- Reason:', error.details.reason);
console.log('- Daily limit:', error.details.limits.dailyLimit);
console.log('- Remaining:', 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);