Dynamic.xyz Adapter
Use SpendSafe with Dynamic.xyz, the embedded wallet infrastructure with social login and MPC security.
Overview
Best for: Consumer-facing apps, social login (email/Google/Twitter), embedded wallets Chains: All EVM-compatible blockchains Custody model: MPC embedded wallets (keys split between user device + Dynamic servers) Bundle size: ~250kB Features: ERC-4337 smart wallets, social auth, wallet recovery
Installation
npm install @spendsafe/sdk @dynamic-labs/sdk-react-core @dynamic-labs/ethereum
Peer dependencies:
@dynamic-labs/sdk-react-corev2.x@dynamic-labs/ethereumv2.xreactv18.x (for frontend integration)
Basic Setup
1. Get Dynamic API key
- Sign up at https://app.dynamic.xyz/
- Create a new project
- Copy your Environment ID from the dashboard
- Configure allowed domains and wallet settings
2. Frontend Setup (React)
import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core';
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
function App() {
return (
<DynamicContextProvider
settings={{
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
walletConnectors: [EthereumWalletConnectors],
}}>
<YourApp />
</DynamicContextProvider>
);
}
3. Backend Setup (Agent)
import { PolicyWallet, createDynamicAdapter } from '@spendsafe/sdk';
// After user authenticates in frontend, get auth token
const authToken = req.headers.authorization; // From frontend
// Create adapter with user's embedded wallet
const adapter = await createDynamicAdapter(authToken, 'base');
// Wrap with PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY,
});
4. 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 - Dynamic.xyz credentials
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=... # Your Dynamic Environment ID (frontend)
DYNAMIC_API_SECRET=... # Your API secret (backend only)
# Required - SpendSafe API key
SPENDSAFE_API_KEY=sk_...
# Optional
DYNAMIC_NETWORK=base # Default network (base, ethereum, polygon, etc.)
Dynamic Dashboard Configuration
Configure in https://app.dynamic.xyz/:
-
Wallet Settings:
- Enable embedded wallets (MPC)
- Choose social providers (email, Google, Twitter, etc.)
- Configure smart wallet features (ERC-4337)
-
Security:
- Add allowed domains
- Set authentication timeout
- Configure MFA requirements
-
Networks:
- Enable chains you want to support
- Set default network
Social Login Providers
Enable social auth in Dynamic dashboard:
- Email: Magic link authentication
- Google: OAuth login
- Twitter: OAuth login
- Discord: OAuth login
- Apple: Sign in with Apple
- Farcaster: Web3-native social login
Complete Example
Frontend (React)
import { DynamicContextProvider, useDynamicContext } from '@dynamic-labs/sdk-react-core';
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
// App wrapper
function App() {
return (
<DynamicContextProvider
settings={{
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!,
walletConnectors: [EthereumWalletConnectors],
}}>
<PaymentComponent />
</DynamicContextProvider>
);
}
// Component that uses wallet
function PaymentComponent() {
const { primaryWallet, user } = useDynamicContext();
async function handlePayment() {
if (!primaryWallet || !user) {
console.log('User not authenticated');
return;
}
// Get auth token
const authToken = await user.getAuthToken();
// Send to your backend
const response = await fetch('/api/process-payment', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000', // 1 USDC
}),
});
const result = await response.json();
console.log('Transaction:', result.hash);
}
return (
<button onClick={handlePayment}>
Send Payment
</button>
);
}
Backend (Express)
import { PolicyWallet, createDynamicAdapter } from '@spendsafe/sdk';
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/process-payment', async (req, res) => {
try {
// 1. Get auth token from frontend
const authToken = req.headers.authorization?.replace('Bearer ', '');
if (!authToken) {
return res.status(401).json({ error: 'Unauthorised' });
}
// 2. Create Dynamic adapter
const adapter = await createDynamicAdapter(authToken, '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: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC Base
});
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 {
res.status(500).json({ error: error.message });
}
}
});
app.listen(3000);
Advanced Usage
Smart Wallet Features (ERC-4337)
Dynamic supports smart wallets with advanced features:
// Enable smart wallet features in Dynamic dashboard
// Then transactions automatically use smart wallet capabilities:
// - Account abstraction (ERC-4337)
// - Batched transactions
// - Sponsored gas (if configured)
// - Social recovery
Multi-Chain Support
// Base wallet
const baseAdapter = await createDynamicAdapter(authToken, 'base');
const baseWallet = new PolicyWallet(baseAdapter, { apiKey: 'key-1' });
// Ethereum wallet
const ethAdapter = await createDynamicAdapter(authToken, 'ethereum');
const ethWallet = new PolicyWallet(ethAdapter, { apiKey: 'key-2' });
// Users can switch chains seamlessly
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
res.status(403).json({
error: 'Policy blocked transaction',
reason: error.details.reason,
dailyLimit: error.details.limits.dailyLimit,
remainingDaily: error.details.counters.remainingDaily,
});
} else if (error.message.includes('User not authenticated')) {
// Authentication error
res.status(401).json({ error: 'Please log in' });
} else if (error.message.includes('insufficient funds')) {
// Balance error
res.status(400).json({ error: 'Insufficient balance' });
} else {
// Other error
res.status(500).json({ error: error.message });
}
}
Get User Information
// Frontend - Get user details
const { user, primaryWallet } = useDynamicContext();
console.log('User email:', user?.email);
console.log('Wallet address:', primaryWallet?.address);
console.log('Auth method:', user?.verifiedCredentials[0].format); // email, oauth, etc.
// Backend - Verify user from auth token
const adapter = await createDynamicAdapter(authToken, 'base');
const address = await adapter.getAddress();
console.log('User wallet:', address);
Wallet Recovery
Dynamic handles wallet recovery automatically:
- Email recovery: User can recover via email link
- Social recovery: Recover using social login
- MPC security: Keys reconstructed from shards
No action needed from your application - Dynamic handles this.
Network Configuration
Supported Networks
Dynamic supports all EVM chains:
// Ethereum
const adapter = await createDynamicAdapter(authToken, 'ethereum');
// Base (recommended for low fees)
const adapter = await createDynamicAdapter(authToken, 'base');
// Polygon
const adapter = await createDynamicAdapter(authToken, 'polygon');
// Arbitrum
const adapter = await createDynamicAdapter(authToken, 'arbitrum');
// Optimism
const adapter = await createDynamicAdapter(authToken, 'optimism');
// Avalanche
const adapter = await createDynamicAdapter(authToken, 'avalanche');
Network Selection in Dashboard
Configure enabled networks in Dynamic dashboard:
- Go to https://app.dynamic.xyz/
- Navigate to Configurations > Networks
- Enable desired networks
- Set default network
Troubleshooting
"User not authenticated" error
Problem: Auth token is invalid or expired
Solution: Ensure user is logged in and token is fresh:
// Frontend - Check authentication
const { isAuthenticated, user } = useDynamicContext();
if (!isAuthenticated) {
// Show login modal
return;
}
// Get fresh token
const authToken = await user.getAuthToken();
"Invalid environment ID" error
Problem: Dynamic Environment ID is incorrect
Solution: Verify Environment ID from Dynamic dashboard:
// ✅ Correct
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=abc-123-def-456
// ❌ Wrong - API secret (don't use client-side!)
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=sk_live_...
"Wallet not initialized" error
Problem: User hasn't created embedded wallet yet
Solution: Ensure user completes onboarding flow:
const { primaryWallet } = useDynamicContext();
if (!primaryWallet) {
// User needs to complete wallet setup
console.log('Please complete wallet setup');
return;
}
"Network not supported" error
Problem: Network not enabled in Dynamic dashboard
Solution: Enable network in Dynamic dashboard:
- Go to Configurations > Networks
- Enable the network you want to use
- Save changes
CORS errors
Problem: Dynamic requests blocked by CORS
Solution: Add your domain to allowed origins:
- Go to Dynamic dashboard > Settings > Allowed Origins
- Add your frontend domain (e.g.,
https://yourapp.com) - For development, add
http://localhost:3000
Best Practises
1. Never expose API secret client-side
// ✅ Good - Environment ID is public
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=abc-123
// ❌ NEVER expose API secret in frontend
// DYNAMIC_API_SECRET should only be used server-side
2. Validate auth tokens server-side
// Always verify auth token on backend before processing
async function verifyToken(authToken: string) {
try {
const adapter = await createDynamicAdapter(authToken, 'base');
const address = await adapter.getAddress();
return { valid: true, address };
} catch {
return { valid: false };
}
}
3. Handle social login gracefully
// Check which auth method user prefers
const { user } = useDynamicContext();
const authMethod = user?.verifiedCredentials[0].format;
switch (authMethod) {
case 'email':
console.log('User logged in with email');
break;
case 'oauth':
console.log('User logged in with social (Google/Twitter)');
break;
}
4. Use testnet for development
// Development: Enable testnets in Dynamic dashboard
// - Base Sepolia
// - Ethereum Sepolia
// - Polygon Mumbai
// Production: Use mainnets only
// - Base
// - Ethereum
// - Polygon
5. Implement proper error boundaries
// Frontend - Wrap app with error boundary
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
<DynamicContextProvider settings={...}>
<App />
</DynamicContextProvider>
</ErrorBoundary>
Why Choose Dynamic.xyz?
Social Login
// Users can log in with:
// - Email (magic link)
// - Google OAuth
// - Twitter OAuth
// - Discord, Apple, Farcaster, etc.
// No seed phrases, no wallet setup - just social login
Embedded Wallets
- MPC security: Keys split between user device + Dynamic servers
- No seed phrases: Users don't need to manage keys
- Recovery built-in: Recover wallet via email/social login
- Mobile-friendly: Works seamlessly on mobile devices
Smart Wallet Support (ERC-4337)
// Enable in Dynamic dashboard for:
// - Account abstraction
// - Batched transactions
// - Sponsored gas
// - Session keys
// - Social recovery
Developer Experience
// Simple React integration
<DynamicContextProvider settings={...}>
<App />
</DynamicContextProvider>
// Easy backend integration
const adapter = await createDynamicAdapter(authToken, 'base');
Comparison with Other Adapters
| Feature | Dynamic.xyz | Privy | ethers.js |
|---|---|---|---|
| Social login | ✅ | ✅ | ❌ |
| Embedded wallets | ✅ | ✅ | ❌ |
| MPC security | ✅ | ✅ | ❌ |
| Smart wallets | ✅ | ⚠️ Limited | ❌ |
| Self-custody | ⚠️ MPC | ⚠️ MPC | ✅ |
| Setup complexity | Medium | Easy | Easy |
| Best for | Consumer apps | Simple embedded | Full custody |
Use Dynamic.xyz if:
- You need social login (email, Google, Twitter)
- You want embedded wallets with MPC
- You need smart wallet features (ERC-4337)
- You're building consumer-facing applications
Use Privy if:
- You want simpler embedded wallet setup
- You don't need advanced smart wallet features
- You prefer lighter-weight SDK
Use ethers.js if:
- You need full self-custody
- You're building for technical users
- Social login isn't required
Next Steps
API Reference
createDynamicAdapter()
function createDynamicAdapter(
authToken: string,
network: string
): Promise<WalletAdapter>
Parameters:
authToken- Dynamic auth token from authenticated user (obtained viauser.getAuthToken())network- Network identifier ('base', 'ethereum', 'polygon', etc.)
Returns: Promise resolving to WalletAdapter
Example:
// Frontend - Get auth token
const { user } = useDynamicContext();
const authToken = await user.getAuthToken();
// Backend - Create adapter
const adapter = await createDynamicAdapter(authToken, 'base');
// Use with PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY,
});
Frontend Hooks
import { useDynamicContext } from '@dynamic-labs/sdk-react-core';
// Get user and wallet info
const {
isAuthenticated, // Boolean - is user logged in
user, // User object with email, verifiedCredentials
primaryWallet, // Primary wallet with address
wallets, // All connected wallets
} = useDynamicContext();
// Get auth token
const authToken = await user.getAuthToken();
// Get wallet address
const address = primaryWallet?.address;