Skip to main content

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:

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

NetworkChain IDRPC URL
Mainnet1https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY
Sepolia11155111https://eth-sepolia.g.alchemy.com/v2/YOUR-KEY

Base Networks

NetworkChain IDRPC URL
Base Mainnet8453https://mainnet.base.org
Base Sepolia84532https://sepolia.base.org

Other EVM Networks

ethers.js works with any EVM chain. Just provide the correct RPC URL:

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'
);