Coinbase SDK Adapter
Use SpendSafe with Coinbase Developer Platform SDK, the enterprise-grade infrastructure for onchain applications.
Overview
Best for: Enterprise applications, gasless USDC transfers, multi-chain support Chains: Ethereum, Base, Polygon (Arbitrum/Optimism on the roadmap) Custody model: Developer-custodied wallets (keys managed by Coinbase) Bundle size: ~150kB
Installation
npm install @spendsafe/sdk @coinbase/coinbase-sdk
Peer dependencies: @coinbase/coinbase-sdk v0.x
Basic Setup
1. Get Coinbase API credentials
- Sign up at https://portal.cdp.coinbase.com/
- Create a new project
- Generate API key credentials (download JSON file)
- Fund your wallet with USDC (for gasless transfers) or ETH (for gas fees)
2. Import the adapter
import { PolicyWallet, createCoinbaseAdapter, getNetworkId } from '@spendsafe/sdk';
import { Coinbase, Wallet } from '@coinbase/coinbase-sdk';
3. Create the adapter
// Initialise Coinbase SDK
Coinbase.configure({
apiKeyName: process.env.COINBASE_API_KEY_NAME!,
privateKey: process.env.COINBASE_PRIVATE_KEY!,
});
// Create or import wallet
const user = await Coinbase.defaultUser();
const wallet = await user.createWallet(); // or user.importWallet(...)
// Create adapter (pick chain + network)
const networkId = getNetworkId('base', 'testnet'); // use 'mainnet' for production
const adapter = await createCoinbaseAdapter(wallet, networkId);
// You can also pass wallet.export() here when restoring from stored wallet data.
4. Wrap with PolicyWallet
const policyWallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY,
});
5. Send transactions
// Gasless USDC transfer on Base
await policyWallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000', // 1 USDC (6 decimals)
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
});
// Native ETH transfer (requires gas)
await policyWallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000000000000000', // 1 ETH in wei
});
Configuration
Environment Variables
# Required - Coinbase API credentials
COINBASE_API_KEY_NAME=organizations/.../apiKeys/...
COINBASE_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n"
# Required - SpendSafe API key
SPENDSAFE_API_KEY=sk_...
# Optional - Network preference
COINBASE_NETWORK=base # or ethereum, polygon, etc.
Supported Networks
import { createCoinbaseAdapter, getNetworkId } from '@spendsafe/sdk';
// Base (recommended for gasless USDC)
const baseSepolia = getNetworkId('base', 'testnet');
const baseMainnet = getNetworkId('base', 'mainnet');
const baseTestnetAdapter = await createCoinbaseAdapter(wallet, baseSepolia);
const baseMainnetAdapter = await createCoinbaseAdapter(wallet, baseMainnet);
// Ethereum
const ethereumMainnet = getNetworkId('ethereum', 'mainnet');
const ethereumSepolia = getNetworkId('ethereum', 'testnet');
const ethAdapter = await createCoinbaseAdapter(wallet, ethereumMainnet);
const ethTestnetAdapter = await createCoinbaseAdapter(wallet, ethereumSepolia);
// Polygon
const polygonMainnet = getNetworkId('polygon', 'mainnet');
const polygonMumbai = getNetworkId('polygon', 'testnet');
const polygonAdapter = await createCoinbaseAdapter(wallet, polygonMainnet);
const polygonTestnetAdapter = await createCoinbaseAdapter(wallet, polygonMumbai);
Coinbase's SDK doesn't expose Optimism/Arbitrum network IDs yet. As soon as CDP promotes them we will wire the adapters and update this guide.
Adapter Options
createCoinbaseAdapter, createCoinbaseEVMAdapter, and createNewCoinbaseWallet accept an optional third argument to fine-tune behaviour:
| Option | Description |
|---|---|
defaultAssetId | Asset ID to read balances from when getBalance() has no token parameter (defaults to eth). |
defaultAssetDecimals | Override decimal precision for the default asset. |
publicClient | Inject your own viem PublicClient (recommended when you already have an RPC connection with auth headers). |
rpcUrl | Custom RPC endpoint used when a public client isn't injected. |
pollingIntervalMs | How frequently to poll for a transaction hash after submitting to Coinbase (default 1500). |
maxFetchAttempts | How many polling attempts to make before timing out (default 10). |
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
const publicClient = createPublicClient({
chain: base,
transport: http(process.env.BASE_RPC_URL!),
});
const adapter = await createCoinbaseAdapter(wallet.export(), baseMainnet, {
publicClient,
pollingIntervalMs: 500,
maxFetchAttempts: 20,
});
Wallet Setup
Create new wallet:
const user = await Coinbase.defaultUser();
const wallet = await user.createWallet();
// Export wallet seed for backup
const seed = wallet.export();
console.log('Backup seed:', seed); // Store securely!
Import existing wallet:
const user = await Coinbase.defaultUser();
const wallet = await user.importWallet({
seed: process.env.WALLET_SEED,
walletId: process.env.WALLET_ID,
});
List existing wallets:
const user = await Coinbase.defaultUser();
const wallets = await user.listWallets();
console.log('Your wallets:', wallets);
Complete Example
import { PolicyWallet, createCoinbaseAdapter, getNetworkId } from '@spendsafe/sdk';
import { Coinbase } from '@coinbase/coinbase-sdk';
import * as dotenv from 'dotenv';
dotenv.config();
async function main() {
// 1. Configure Coinbase SDK
Coinbase.configure({
apiKeyName: process.env.COINBASE_API_KEY_NAME!,
privateKey: process.env.COINBASE_PRIVATE_KEY!,
});
// 2. Get or create wallet
const user = await Coinbase.defaultUser();
const wallet = await user.createWallet(); // or importWallet()
console.log('Wallet address:', wallet.getDefaultAddress()?.getId());
// 3. Create Coinbase adapter
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'testnet'));
// 4. Create PolicyWallet
const policyWallet = new PolicyWallet(adapter, {
apiKey: process.env.SPENDSAFE_API_KEY!,
});
try {
// 5. Send gasless USDC transfer
const result = await policyWallet.send({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1',
amount: '1000000', // 1 USDC
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
});
console.log('✅ Transaction sent:', result.hash);
console.log('⛽ Gas fee:', result.fee); // $0 for gasless USDC
} 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
Gasless USDC Transfers
Coinbase SDK supports gasless transfers for USDC on Base:
// No gas required for USDC transfers on Base!
await policyWallet.send({
to: recipient,
amount: '5000000', // 5 USDC
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
});
console.log('Gas fee:', result.fee); // "0" - completely gasless!
Requirements:
- Must be USDC token
- Must be on Base network
- Wallet must have USDC balance
Multi-Chain Operations
import { getNetworkId } from '@spendsafe/sdk';
// Base wallet (gasless USDC)
const baseAdapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'mainnet'));
const baseWallet = new PolicyWallet(baseAdapter, { apiKey: 'key-1' });
// Ethereum wallet (standard gas)
const ethAdapter = await createCoinbaseAdapter(wallet, getNetworkId('ethereum', 'mainnet'));
const ethWallet = new PolicyWallet(ethAdapter, { apiKey: 'key-2' });
// Send on different chains
await baseWallet.send({ to: '0x...', amount: '1000000', asset: USDC_BASE });
await ethWallet.send({ to: '0x...', amount: '1000000000000000000' }); // ETH
Error Handling
import { PolicyError } from '@spendsafe/sdk';
try {
await policyWallet.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('insufficient funds')) {
// Not enough balance
console.log('Insufficient balance');
} else if (error.message.includes('Wallet not found')) {
// Wallet doesn't exist
console.log('Wallet not found - check wallet ID');
} else {
// Other error (network, API, etc.)
console.error('Transaction failed:', error.message);
}
}
Balance Checks
// Get wallet balance
const balance = await adapter.getBalance();
console.log('Wallet balance:', balance, 'wei');
// Check USDC balance (requires @coinbase/coinbase-sdk methods)
const usdcBalance = await wallet.getBalance('usdc');
console.log('USDC balance:', usdcBalance); // e.g., "10.50"
Wallet Management
// Export wallet (for backup)
const seed = wallet.export();
// Store seed securely - required to restore wallet
// Get wallet ID
const walletId = wallet.getId();
console.log('Wallet ID:', walletId);
// Get default address
const address = wallet.getDefaultAddress()?.getId();
console.log('Address:', address);
// List all addresses in wallet
const addresses = await wallet.listAddresses();
console.log('Addresses:', addresses);
Funding Wallets
Option 1: Faucet (Testnet only)
// Request testnet funds from Coinbase faucet
const faucet = await wallet.faucet();
console.log('Faucet transaction:', faucet);
Option 2: Transfer from External Wallet
# Send USDC to your Coinbase wallet address
# Address: wallet.getDefaultAddress()?.getId()
Option 3: Coinbase Account
// Transfer from your Coinbase account to wallet
// (requires additional Coinbase account integration)
Network Configuration
Base (Recommended)
| Feature | Details |
|---|---|
| Chain ID | 8453 |
| Gas | Very low (~$0.01) |
| USDC transfers | Gasless! |
| Best for | USDC payments, low-cost transactions |
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'mainnet'));
Ethereum Mainnet
| Feature | Details |
|---|---|
| Chain ID | 1 |
| Gas | High (~$5-50) |
| USDC transfers | Requires gas |
| Best for | Maximum security, established ecosystem |
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('ethereum', 'mainnet'));
Layer 2 Networks
| Network | Chain ID | Gas Costs | Best For |
|---|---|---|---|
| Base | 8453 | Very low | Gasless USDC, enterprise flows |
| Polygon | 137 | Very low | High-volume transactions |
Additional L2s (Optimism, Arbitrum) will be documented once Coinbase exposes official SDK network IDs.
Token Addresses by Network
USDC:
- Base:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 - Ethereum:
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 - Polygon:
0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
Troubleshooting
"Invalid API credentials" error
Problem: API key or private key is incorrect
Solution: Verify credentials from Coinbase portal:
// ✅ Correct format
COINBASE_API_KEY_NAME=organizations/abc-123/apiKeys/def-456
COINBASE_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\nMH...\n-----END EC PRIVATE KEY-----\n"
// ❌ Wrong - missing quotes or newlines
COINBASE_PRIVATE_KEY=-----BEGIN EC PRIVATE KEY-----...
"Wallet not found" error
Problem: Wallet ID doesn't exist or was deleted
Solution: Create new wallet or verify wallet ID:
// List all wallets
const user = await Coinbase.defaultUser();
const wallets = await user.listWallets();
console.log('Available wallets:', wallets);
// Create new wallet if needed
const wallet = await user.createWallet();
"Insufficient funds" error
Problem: Wallet doesn't have enough balance
Solution: Fund your wallet:
// Check balance first
const balance = await wallet.getBalance('usdc');
console.log('USDC balance:', balance);
// Fund wallet via:
// 1. Coinbase faucet (testnet)
// 2. Transfer from external wallet
// 3. Coinbase account integration
"Gas estimation failed" error
Problem: Transaction would fail or gas too high
Solution: For USDC on Base, use gasless transfers:
// ✅ Gasless on Base
await policyWallet.send({
to: recipient,
amount: '1000000',
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC Base
});
// If using other chains, ensure ETH for gas
const ethBalance = await wallet.getBalance('eth');
console.log('ETH balance:', ethBalance);
"Rate limit exceeded" error
Problem: Too many API requests
Solution: Implement exponential backoff:
async function sendWithRetry(wallet: PolicyWallet, params: any, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await wallet.send(params);
} catch (error) {
if (error.message.includes('rate limit') && i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
continue;
}
throw error;
}
}
}
Best Practises
1. Use Base for USDC payments
// ✅ Good - Gasless USDC on Base
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'mainnet'));
// ❌ Expensive - USDC on Ethereum requires gas
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('ethereum', 'mainnet'));
2. Back up wallet seeds
// Always export and store wallet seed securely
const seed = wallet.export();
// Store in:
// - Encrypted vault (1Password, Vault)
// - Secure environment variable
// - Hardware security module (HSM)
// ❌ NEVER commit to git or store in plain text
3. Use testnet for development
// Development: Use testnet (request faucet funds)
Coinbase.configure({
apiKeyName: process.env.COINBASE_API_KEY_NAME_TESTNET!,
privateKey: process.env.COINBASE_PRIVATE_KEY_TESTNET!,
networkId: 'base-sepolia', // Testnet
});
// Production: Use mainnet
Coinbase.configure({
apiKeyName: process.env.COINBASE_API_KEY_NAME!,
privateKey: process.env.COINBASE_PRIVATE_KEY!,
});
4. Handle errors gracefully
try {
await policyWallet.send({ to, amount, token });
} catch (error) {
// Log error for monitoring
console.error('Transaction failed:', error);
// Notify admin for policy violations
if (error instanceof PolicyError) {
await notifyAdmin(`Policy blocked: ${error.details.reason}`);
}
}
5. Monitor gasless transfer availability
// Gasless transfers may not always be available
// Always have fallback ETH for gas
const balance = await wallet.getBalance('eth');
if (Number(balance) < 0.001) {
console.warn('Low ETH balance - gasless transfers may fail');
}
Why Choose Coinbase SDK?
Enterprise Infrastructure
- Reliability: Built by Coinbase, battle-tested at scale
- Compliance: Meets regulatory requirements
- Support: Enterprise-grade support available
- Security: Industry-leading key management
Gasless Transfers
// Save 100% on gas fees for USDC on Base
await wallet.send({
to: recipient,
amount: '1000000',
asset: USDC_BASE,
});
// Gas fee: $0 (completely gasless!)
Multi-Chain Support
Single SDK for multiple chains:
- Ethereum (mainnet, Sepolia)
- Base (mainnet, Sepolia)
- Polygon
- Arbitrum
- Optimism
Developer Experience
// Simple wallet creation
const wallet = await user.createWallet();
// Easy balance checks
const balance = await wallet.getBalance('usdc');
// Built-in faucet for testnet
await wallet.faucet();
Comparison with Other Adapters
| Feature | Coinbase SDK | ethers.js | viem |
|---|---|---|---|
| Custody | Developer-custodied | Self-custodied | Self-custodied |
| Gasless transfers | ✅ USDC on Base | ❌ | ❌ |
| Multi-chain | Built-in | Manual setup | Manual setup |
| Enterprise support | ✅ | Community | Community |
| Setup complexity | Medium | Easy | Easy |
| Bundle size | ~150kB | ~300kB | ~35kB |
Use Coinbase SDK if:
- You want gasless USDC transfers
- You need enterprise support
- You're okay with developer-custodied keys
- You want built-in multi-chain support
Use ethers.js/viem if:
- You need full key custody
- You want self-hosted infrastructure
- Bundle size is critical (viem)
Next Steps
API Reference
createCoinbaseAdapter()
function createCoinbaseAdapter(
walletOrData: Wallet | WalletData,
networkId: CoinbaseNetworkId,
options?: CoinbaseAdapterOptions
): Promise<WalletAdapter>
Parameters:
walletOrData- Coinbase Wallet instance or the exportedwallet.export()payloadnetworkId- Network identifier ('base-mainnet','base-sepolia','polygon-mainnet', etc.). UsegetNetworkId(chain, network)for ergonomics.options- Optional adapter overrides (custom public client, polling interval, etc.)
Returns: Promise resolving to WalletAdapter
Example:
import { Coinbase } from '@coinbase/coinbase-sdk';
import { getNetworkId, createCoinbaseAdapter } from '@spendsafe/sdk';
Coinbase.configure({
apiKeyName: process.env.COINBASE_API_KEY_NAME!,
privateKey: process.env.COINBASE_PRIVATE_KEY!,
});
const user = await Coinbase.defaultUser();
const wallet = await user.createWallet();
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'testnet'));