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-authv1.x
Basic Setup
1. Get Privy API credentials
- Sign up at https://dashboard.privy.io/
- Create a new application
- Copy your App ID and App Secret from Settings
- 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/:
-
Authentication:
- Enable email login (magic link)
- Enable social logins (Google, Twitter, Discord, etc.)
- Configure session duration
-
Embedded Wallets:
- Enable embedded wallet creation
- Choose networks to support
- Configure wallet security settings
-
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:
- Go to https://dashboard.privy.io/
- Navigate to Settings > Embedded Wallets
- Enable desired networks
- 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:
- Go to Settings > Embedded Wallets
- Enable the network
- 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
| Feature | Privy | Dynamic.xyz | ethers.js |
|---|---|---|---|
| Server-side focus | ✅ | ⚠️ | ✅ |
| Embedded wallets | ✅ | ✅ | ❌ |
| Social login | ✅ | ✅ | ❌ |
| Smart wallets | ❌ | ✅ | ❌ |
| Setup complexity | Easy | Medium | Easy |
| Bundle size | ~100kB | ~250kB | ~300kB |
| Best for | Backend agents | Consumer apps | Full 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);