Skip to main content

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

  1. Sign up at https://portal.cdp.coinbase.com/
  2. Create a new project
  3. Generate API key credentials (download JSON file)
  4. 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:

OptionDescription
defaultAssetIdAsset ID to read balances from when getBalance() has no token parameter (defaults to eth).
defaultAssetDecimalsOverride decimal precision for the default asset.
publicClientInject your own viem PublicClient (recommended when you already have an RPC connection with auth headers).
rpcUrlCustom RPC endpoint used when a public client isn't injected.
pollingIntervalMsHow frequently to poll for a transaction hash after submitting to Coinbase (default 1500).
maxFetchAttemptsHow 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

FeatureDetails
Chain ID8453
GasVery low (~$0.01)
USDC transfersGasless!
Best forUSDC payments, low-cost transactions
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('base', 'mainnet'));

Ethereum Mainnet

FeatureDetails
Chain ID1
GasHigh (~$5-50)
USDC transfersRequires gas
Best forMaximum security, established ecosystem
const adapter = await createCoinbaseAdapter(wallet, getNetworkId('ethereum', 'mainnet'));

Layer 2 Networks

NetworkChain IDGas CostsBest For
Base8453Very lowGasless USDC, enterprise flows
Polygon137Very lowHigh-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

FeatureCoinbase SDKethers.jsviem
CustodyDeveloper-custodiedSelf-custodiedSelf-custodied
Gasless transfers✅ USDC on Base
Multi-chainBuilt-inManual setupManual setup
Enterprise supportCommunityCommunity
Setup complexityMediumEasyEasy
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 exported wallet.export() payload
  • networkId - Network identifier ('base-mainnet', 'base-sepolia', 'polygon-mainnet', etc.). Use getNetworkId(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'));