ethers.js Adapter
Use SpendSafe with ethers.js, the most popular Ethereum library.
Overview
Best for: Most developers, production applications, maximum compatibility Chains: All EVM-compatible blockchains Versions: Supports both ethers v5 and v6 Bundle size: ~300kB
Installation
npm install @spendsafe/sdk ethers
Peer dependencies: ethers.js v5 or v6
Basic Setup
1. Import the adapter
import { PolicyWallet, createEthersAdapter } from '@spendsafe/sdk';
import { ethers } from 'ethers';
2. Create the adapter
const privateKey = process.env.PRIVATE_KEY; // Your wallet private key
const rpcUrl = process.env.RPC_URL; // e.g., https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY
const adapter = await createEthersAdapter(privateKey, rpcUrl);
3. Wrap with PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY,
});
4. Send transactions
// Native ETH transfer
await wallet.send({
chain: 'ethereum',
asset: 'eth',
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000000000000000', // 1 ETH in wei
});
// ERC-20 token transfer (use contract address as asset)
await wallet.send({
chain: 'ethereum',
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC contract address
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000', // 1 USDC (6 decimals)
});
Configuration
Environment Variables
# Required
PRIVATE_KEY=0x... # Your wallet private key (with 0x prefix)
RPC_URL=https://... # Your RPC endpoint
SPENDSAFE_API_KEY=sk_... # Your SpendSafe API key
# Optional
CHAIN_ID=1 # Ethereum mainnet (auto-detected if not specified)
RPC Providers
Recommended providers:
- Alchemy - https://www.alchemy.com/
- Infura - https://www.infura.io/
- QuickNode - https://www.quicknode.com/
- Public RPCs - https://chainlist.org/ (not recommended for production)
Example RPC URLs:
// Ethereum Mainnet
const rpcUrl = 'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY';
// Ethereum Sepolia (testnet)
const rpcUrl = 'https://eth-sepolia.g.alchemy.com/v2/YOUR-KEY';
// Base Mainnet
const rpcUrl = 'https://mainnet.base.org';
// Base Sepolia
const rpcUrl = 'https://sepolia.base.org';
// Polygon Mainnet
const rpcUrl = 'https://polygon-rpc.com';
Complete Example
import { PolicyWallet, createEthersAdapter } from '@spendsafe/sdk';
import * as dotenv from 'dotenv';
dotenv.config();
async function main() {
// 1. Create ethers.js adapter
const adapter = await createEthersAdapter(
process.env.PRIVATE_KEY!,
process.env.RPC_URL!
);
// 2. Create PolicyWallet
const wallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY!,
});
try {
// 3. Send ETH with policy enforcement
const result = await wallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000000000000000', // 1 ETH
});
console.log('✅ Transaction sent:', result.hash);
console.log('⛽ Gas fee:', result.fee);
} catch (error) {
if (error.code === 'POLICY_VIOLATION') {
console.log('❌ Policy violation:', error.message);
console.log(' Reason:', error.details.reason);
console.log(' Remaining today:', error.details.remainingDaily);
} else {
throw error;
}
}
}
main();
Advanced Usage
ERC-20 Token Transfers
// USDC transfer (6 decimals)
await wallet.send({
to: recipientAddress,
amount: '1000000', // 1 USDC
asset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
});
// DAI transfer (18 decimals)
await wallet.send({
to: recipientAddress,
amount: '1000000000000000000', // 1 DAI
asset: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum
});
Multiple Chains
// Ethereum mainnet wallet
const ethAdapter = await createEthersAdapter(
privateKey,
'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY'
);
const ethWallet = new PolicyWallet(ethAdapter, { apiKey: 'key-1' });
// Base mainnet wallet
const baseAdapter = await createEthersAdapter(
privateKey,
'https://mainnet.base.org'
);
const baseWallet = new PolicyWallet(baseAdapter, { apiKey: 'key-2' });
// Send on different chains
await ethWallet.send({ to: '0x...', amount: '1000000' });
await baseWallet.send({ to: '0x...', amount: '1000000' });
Error Handling
import { PolicyError } from '@spendsafe/sdk';
try {
await wallet.send({ to: merchant, amount: '1000000' });
} 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.code === 'INSUFFICIENT_FUNDS') {
// Not enough balance
console.log('Insufficient funds');
} else {
// Other error (network, gas, etc.)
console.error('Transaction failed:', error.message);
}
}
Gas Estimation
The adapter handles gas estimation automatically, but you can customise:
// Adapter uses ethers.js gas estimation by default
const result = await wallet.send({
to: '0x...',
amount: '1000000',
// Gas estimated automatically
});
console.log('Gas fee paid:', result.fee); // in ETH
Balance Checks
// Get wallet balance
const balance = await adapter.getBalance();
console.log('Wallet balance:', balance, 'wei');
// Convert to human-readable
import { ethers } from 'ethers';
const balanceEth = ethers.formatEther(balance);
console.log('Balance:', balanceEth, 'ETH');
Network Configuration
Ethereum Networks
| Network | Chain ID | RPC URL |
|---|---|---|
| Mainnet | 1 | https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY |
| Sepolia | 11155111 | https://eth-sepolia.g.alchemy.com/v2/YOUR-KEY |
Base Networks
| Network | Chain ID | RPC URL |
|---|---|---|
| Base Mainnet | 8453 | https://mainnet.base.org |
| Base Sepolia | 84532 | https://sepolia.base.org |
Other EVM Networks
ethers.js works with any EVM chain. Just provide the correct RPC URL:
- Polygon: https://polygon-rpc.com
- Optimism: https://mainnet.optimism.io
- Arbitrum: https://arb1.arbitrum.io/rpc
- Avalanche: https://api.avax.network/ext/bc/C/rpc
Troubleshooting
"Invalid private key" error
Problem: Private key format is incorrect
Solution: Ensure private key includes 0x prefix:
// ✅ Correct
const privateKey = '0xabc123...';
// ❌ Wrong
const privateKey = 'abc123...';
"Insufficient funds" error
Problem: Wallet doesn't have enough ETH for transaction + gas
Solution: Fund your wallet with ETH for gas fees:
// Check balance first
const balance = await adapter.getBalance();
console.log('Balance (wei):', balance);
// Ensure balance > amount + gas fees
"Network mismatch" error
Problem: RPC URL doesn't match expected chain
Solution: Verify RPC URL matches your intended network:
// Check what chain you're connected to
const chainId = await adapter.getChainId();
console.log('Connected to chain:', chainId);
// 1 = Ethereum, 8453 = Base, etc.
"Rate limit exceeded" error
Problem: Free RPC tier has rate limits
Solution: Use a paid RPC provider (Alchemy, Infura) or implement retries:
// Upgrade to paid tier, or add retry logic
const adapter = await createEthersAdapter(privateKey, rpcUrl);
Best Practises
1. Use environment variables for secrets
// ✅ Good
const privateKey = process.env.PRIVATE_KEY;
// ❌ Never hardcode
const privateKey = '0xabc123...'; // DON'T DO THIS
2. Use testnet for development
// Development: Use Sepolia testnet
const rpcUrl = 'https://eth-sepolia.g.alchemy.com/v2/YOUR-KEY';
// Production: Use mainnet
const rpcUrl = 'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY';
3. Handle policy violations gracefully
try {
await wallet.send({ to, amount });
} catch (error) {
if (error.code === 'POLICY_VIOLATION') {
// Log and notify, don't crash
await notifyAdmin(`Policy blocked: ${error.details.reason}`);
}
}
4. Monitor gas fees
const result = await wallet.send({ to, amount });
console.log('Gas fee:', result.fee); // Monitor costs
Next Steps
API Reference
createEthersAdapter()
function createEthersAdapter(
privateKey: string,
rpcUrl: string
): Promise<WalletAdapter>
Parameters:
privateKey- Wallet private key (with 0x prefix)rpcUrl- Ethereum RPC endpoint URL
Returns: Promise resolving to WalletAdapter
Example:
const adapter = await createEthersAdapter(
'0xabc123...',
'https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY'
);