Skip to main content

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-core v2.x
  • @dynamic-labs/ethereum v2.x
  • react v18.x (for frontend integration)

Basic Setup

1. Get Dynamic API key

  1. Sign up at https://app.dynamic.xyz/
  2. Create a new project
  3. Copy your Environment ID from the dashboard
  4. 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/:

  1. Wallet Settings:

    • Enable embedded wallets (MPC)
    • Choose social providers (email, Google, Twitter, etc.)
    • Configure smart wallet features (ERC-4337)
  2. Security:

    • Add allowed domains
    • Set authentication timeout
    • Configure MFA requirements
  3. 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:

  1. Email recovery: User can recover via email link
  2. Social recovery: Recover using social login
  3. 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:

  1. Go to https://app.dynamic.xyz/
  2. Navigate to Configurations > Networks
  3. Enable desired networks
  4. 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:

  1. Go to Configurations > Networks
  2. Enable the network you want to use
  3. Save changes

CORS errors

Problem: Dynamic requests blocked by CORS

Solution: Add your domain to allowed origins:

  1. Go to Dynamic dashboard > Settings > Allowed Origins
  2. Add your frontend domain (e.g., https://yourapp.com)
  3. 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

FeatureDynamic.xyzPrivyethers.js
Social login
Embedded wallets
MPC security
Smart wallets⚠️ Limited
Self-custody⚠️ MPC⚠️ MPC
Setup complexityMediumEasyEasy
Best forConsumer appsSimple embeddedFull 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 via user.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;