Coinbase Developer Platform (CDP) TypeScript SDK
Table of Contents
[!TIP]
If you're looking to contribute to the SDK, please see the Contributing Guide.
CDP SDK
This module contains the TypeScript CDP SDK, which is a library that provides a client for interacting with the Coinbase Developer Platform (CDP). It includes a CDP Client for interacting with EVM and Solana APIs to create accounts and send transactions, policy APIs to govern transaction permissions, as well as authentication tools for interacting directly with the CDP APIs.
Documentation
CDP SDK has auto-generated docs for the Typescript SDK.
Further documentation is also available on the CDP docs website:
Installation
npm install @coinbase/cdp-sdk
API Keys
To start, create a CDP API Key. Save the API Key ID and API Key Secret for use in the SDK. You will also need to create a wallet secret in the Portal to sign transactions.
Most CDP API endpoints require API key credentials. Public (unauthenticated) endpoints can be called without credentials, and authenticated endpoints will raise a clear request-time error if credentials are missing.
Usage
Initialization
Load client config from shell
One option is to export your CDP API Key and Wallet Secret as environment variables:
export CDP_API_KEY_ID="YOUR_API_KEY_ID"
export CDP_API_KEY_SECRET="YOUR_API_KEY_SECRET"
export CDP_WALLET_SECRET="YOUR_WALLET_SECRET"
Then, initialize the client:
import { CdpClient } from "@coinbase/cdp-sdk";
const cdp = new CdpClient();
If you are only calling public (unauthenticated) endpoints, constructing CdpClient without credentials is supported:
import { CdpClient } from "@coinbase/cdp-sdk";
const cdp = new CdpClient();
Load client config from .env file
Another option is to save your CDP API Key and Wallet Secret in a .env file:
touch .env
echo "CDP_API_KEY_ID=YOUR_API_KEY_ID" >> .env
echo "CDP_API_KEY_SECRET=YOUR_API_KEY_SECRET" >> .env
echo "CDP_WALLET_SECRET=YOUR_WALLET_SECRET" >> .env
Then, load the client config from the .env file:
import { CdpClient } from "@coinbase/cdp-sdk";
import dotenv from "dotenv";
dotenv.config();
const cdp = new CdpClient();
Pass the API Key and Wallet Secret to the client
Another option is to directly pass the API Key and Wallet Secret to the client:
const cdp = new CdpClient({
apiKeyId: "YOUR_API_KEY_ID",
apiKeySecret: "YOUR_API_KEY_SECRET",
walletSecret: "YOUR_WALLET_SECRET",
});
Client Lifecycle
The CDP client wraps an HTTP client (Axios) and should be created once and reused throughout your application's lifecycle. The underlying HTTP client handles connection pooling automatically, so there's no need to recreate the client per request—doing so would be less efficient.
- Long-lived services: Create a single client instance at startup
- Serverless/request-based runtimes: Create once per cold start, or use a module-level singleton
- Concurrency: The client is safe to use across concurrent async operations
Creating EVM or Solana accounts
Create an EVM account as follows:
const account = await cdp.evm.createAccount();
Import an EVM account as follows:
const account = await cdp.evm.importAccount({
privateKey: "0x123456",
name: "MyAccount",
});
Create a Solana account as follows:
const account = await cdp.solana.createAccount();
Import a Solana account as follows:
const account = await cdp.solana.importAccount({
privateKey: "3MLZ...Uko8zz",
name: "MyAccount",
});
Exporting EVM or Solana accounts
Export an EVM account as follows:
const privateKey = await cdp.evm.exportAccount({
name: "MyAccount",
});
const privateKey = await cdp.evm.exportAccount({
address: "0x123",
});
Export a Solana account as follows:
const privateKey = await cdp.solana.exportAccount({
name: "MyAccount",
});
const privateKey = await cdp.solana.exportAccount({
address: "Abc",
});
Get or Create an EVM account as follows:
const account = await cdp.evm.getOrCreateAccount({
name: "Account1",
});
Get or Create a Solana account as follows:
const account = await cdp.solana.getOrCreateAccount({
name: "Account1",
});
Get or Create a Smart Account as follows:
const owner = await cdp.evm.createAccount();
const account = await cdp.evm.getOrCreateSmartAccount({
name: "Account1",
owner,
});
Creating EVM or Solana accounts with policies
Create an EVM account with policy as follows:
const account = await cdp.evm.createAccount({
name: "AccountWithPolicy",
accountPolicy: "abcdef12-3456-7890-1234-567890123456",
});
Create a Solana account with policy as follows:
const account = await cdp.solana.createAccount({
name: "AccountWithPolicy",
accountPolicy: "abcdef12-3456-7890-1234-567890123456",
});
Updating EVM or Solana accounts
Update an EVM account as follows:
const account = await cdp.evm.updateAccount({
addresss: account.address,
update: {
name: "Updated name",
accountPolicy: "1622d4b7-9d60-44a2-9a6a-e9bbb167e412",
},
});
Update a Solana account as follows:
const account = await cdp.solana.updateAccount({
addresss: account.address,
update: {
name: "Updated name",
accountPolicy: "1622d4b7-9d60-44a2-9a6a-e9bbb167e412",
},
});
Testnet faucet
You can use the faucet function to request testnet ETH or SOL from the CDP.
Request testnet ETH as follows:
const faucetResp = await cdp.evm.requestFaucet({
address: evmAccount.address,
network: "base-sepolia",
token: "eth",
});
Request testnet SOL as follows:
const faucetResp = await cdp.solana.requestFaucet({
address: fromAddress,
token: "sol",
});
Sending transactions
EVM
You can use CDP SDK to send transactions on EVM networks.
import { CdpClient } from "@coinbase/cdp-sdk";
import { parseEther, createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const cdp = new CdpClient();
const account = await cdp.evm.createAccount();
const faucetResp = await cdp.evm.requestFaucet({
address: account.address,
network: "base-sepolia",
token: "eth",
});
const faucetTxReceipt = await publicClient.waitForTransactionReceipt({
hash: faucetResp.transactionHash,
});
const { transactionHash } = await cdp.evm.sendTransaction({
address: account.address,
network: "base-sepolia",
transaction: {
to: "0x4252e0c9A3da5A2700e7d91cb50aEf522D0C6Fe8",
value: parseEther("0.000001"),
},
});
await publicClient.waitForTransactionReceipt({ hash: transactionHash });
console.log(
`Transaction confirmed! Explorer link: https://sepolia.basescan.org/tx/${transactionHash}`,
);
CDP SDK is fully viem-compatible, so you can optionally use a walletClient to send transactions.
import { CdpClient } from "@coinbase/cdp-sdk";
import { parseEther, createPublicClient, http, createWalletClient, toAccount } from "viem";
import { baseSepolia } from "viem/chains";
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const cdp = new CdpClient();
const account = await cdp.evm.createAccount();
const faucetResp = await cdp.evm.requestFaucet({
address: account.address,
network: "base-sepolia",
token: "eth",
});
const faucetTxReceipt = await publicClient.waitForTransactionReceipt({
hash: faucetResp.transactionHash,
});
const walletClient = createWalletClient({
account: toAccount(serverAccount),
chain: baseSepolia,
transport: http(),
});
const hash = await walletClient.sendTransaction({
to: "0x4252e0c9A3da5A2700e7d91cb50aEf522D0C6Fe8",
value: parseEther("0.000001"),
});
console.log(`Transaction confirmed! Explorer link: https://sepolia.basescan.org/tx/${hash}`);
Solana
You can use CDP SDK to send transactions on Solana.
For complete examples, check out sendTransaction.ts, sendManyTransactions.ts, and sendManyBatchedTransactions.ts.
import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";
import {
address as solanaAddress,
appendTransactionMessageInstructions,
compileTransaction,
createNoopSigner,
createSolanaRpc,
createTransactionMessage,
getBase64EncodedWireTransaction,
pipe,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const cdp = new CdpClient();
const account = await cdp.solana.createAccount();
await cdp.solana.requestFaucet({
address: account.address,
token: "sol",
});
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const {
value: { blockhash, lastValidBlockHeight },
} = await rpc.getLatestBlockhash().send();
const instruction = getTransferSolInstruction({
source: createNoopSigner(solanaAddress(account.address)),
destination: solanaAddress("3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE"),
amount: 10000n,
});
const txMsg = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(solanaAddress(account.address), tx),
tx => setTransactionMessageLifetimeUsingBlockhash({ blockhash, lastValidBlockHeight }, tx),
tx => appendTransactionMessageInstructions([instruction], tx),
);
const serializedTx = getBase64EncodedWireTransaction(compileTransaction(txMsg));
console.log("Transaction serialized successfully");
const txResult = await cdp.solana.sendTransaction({
network: "solana-devnet",
transaction: serializedTx,
});
console.log(
`Transaction confirmed! Explorer link: https://explorer.solana.com/tx/${txResult.signature}?cluster=devnet`,
);
To have CDP sponsor the transaction fees, pass useCdpSponsor: true. When enabled, CDP pays the network fee so the sender does not need SOL for fees.
const txResult = await cdp.solana.sendTransaction({
network: "solana-devnet",
transaction: serializedTx,
useCdpSponsor: true,
});
EIP-7702 delegation
You can create an EIP-7702 delegation for an existing EOA, upgrading it with smart account capabilities on supported networks. The delegated EOA can then use batched transactions and gas sponsorship via paymaster.
import { CdpClient } from "@coinbase/cdp-sdk";
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";
const cdp = new CdpClient();
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const account = await cdp.evm.getOrCreateAccount({ name: "MyAccount" });
const { delegationOperationId } = await cdp.evm.createEvmEip7702Delegation({
address: account.address,
network: "base-sepolia",
enableSpendPermissions: false,
idempotencyKey: "optional-uuid",
});
const delegationOperation = await cdp.evm.waitForEvmEip7702DelegationOperationStatus({
delegationOperationId,
});
console.log(`Delegation confirmed (status: ${delegationOperation.status})`);
For a runnable example that includes faucet and receipt waiting, see examples/typescript/evm/eip7702/createEip7702Delegation.ts.
EVM Smart Accounts
For EVM, we support Smart Accounts which are account-abstraction (ERC-4337) accounts. Currently there is only support for Base Sepolia and Base Mainnet for Smart Accounts.
Create an EVM account and a smart account as follows:
const evmAccount = await cdp.evm.createAccount();
const smartAccount = await cdp.evm.createSmartAccount({
owner: evmAccount,
});
Sending User Operations
const userOperation = await cdp.evm.sendUserOperation({
smartAccount: smartAccount,
network: "base-sepolia",
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: parseEther("0.000001"),
data: "0x",
},
],
});
In Base Sepolia, all user operations are gasless by default. If you'd like to specify a different paymaster, you can do so as follows:
const userOperation = await cdp.sendUserOperation({
smartAccount: smartAccount,
network: "base-sepolia",
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: parseEther("0"),
data: "0x",
},
],
paymasterUrl: "https://some-paymaster-url.com",
});
EVM Swaps
You can use the CDP SDK to swap tokens on EVM networks using both regular accounts (EOAs) and smart accounts.
The SDK provides three approaches for performing token swaps:
1. All-in-one pattern (Recommended)
The simplest approach for performing swaps. Creates and executes the swap in a single line of code:
Regular Account (EOA):
const account = await cdp.evm.getOrCreateAccount({ name: "MyExistingFundedAccount" });
const { transactionHash } = await account.swap({
network: "base",
toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
fromToken: "0x4200000000000000000000000000000000000006",
fromAmount: BigInt("1000000000000000000"),
slippageBps: 100,
});
console.log(`Swap executed: ${transactionHash}`);
Smart Account:
const owner = await cdp.evm.getOrCreateAccount({ name: "MyOwnerAccount" });
const smartAccount = await cdp.evm.getOrCreateSmartAccount({
name: "MyExistingFundedSmartAccount",
owner,
});
const { userOpHash } = await smartAccount.swap({
network: "base",
toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
fromToken: "0x4200000000000000000000000000000000000006",
fromAmount: BigInt("1000000000000000000"),
slippageBps: 100,
});
console.log(`Smart account swap executed: ${userOpHash}`);
const receipt = await smartAccount.waitForUserOperation({ userOpHash });
console.log(`Status: ${receipt.status}`);
2. Get pricing information
Use getSwapPrice for quick price estimates and display purposes. This is ideal for showing exchange rates without committing to a swap:
const swapPrice = await cdp.evm.getSwapPrice({
network: "ethereum",
toToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
fromToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
fromAmount: BigInt("1000000000000000000"),
taker: "0x1234567890123456789012345678901234567890",
});
if (swapPrice.liquidityAvailable) {
console.log(`You'll receive: ${swapPrice.toAmount} USDC`);
console.log(`Minimum after slippage: ${swapPrice.minToAmount} USDC`);
}
Note: getSwapPrice does not reserve funds or signal commitment to swap, making it suitable for more frequent price updates with less strict rate limiting - although the data may be slightly less precise.
3. Create and execute separately
Use account.quoteSwap() / smartAccount.quoteSwap() when you need full control over the swap process. This returns complete transaction data for execution:
Important: quoteSwap() signals a soft commitment to swap and may reserve funds on-chain. It is rate-limited more strictly than getSwapPrice to prevent abuse.
Regular Account (EOA):
const account = await cdp.evm.getOrCreateAccount({ name: "MyExistingFundedAccount" });
const swapQuote = await account.quoteSwap({
network: "base",
toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
fromToken: "0x4200000000000000000000000000000000000006",
fromAmount: BigInt("1000000000000000000"),
slippageBps: 100,
});
if (!swapQuote.liquidityAvailable) {
console.error("Insufficient liquidity for swap");
return;
}
const { transactionHash } = await swapQuote.execute();
Smart Account:
const owner = await cdp.evm.getOrCreateAccount({ name: "MyOwnerAccount" });
const smartAccount = await cdp.evm.getOrCreateSmartAccount({
name: "MyExistingFundedSmartAccount",
owner,
});
const swapQuote = await smartAccount.quoteSwap({
network: "base",
toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
fromToken: "0x4200000000000000000000000000000000000006",
fromAmount: BigInt("1000000000000000000"),
slippageBps: 100,
});
if (!swapQuote.liquidityAvailable) {
console.error("Insufficient liquidity for swap");
return;
}
const { userOpHash } = await swapQuote.execute();
const receipt = await smartAccount.waitForUserOperation({ userOpHash });
console.log(`Status: ${receipt.status}`);
When to use each approach:
- All-in-one (
account.swap() / smartAccount.swap()): Best for most use cases. Simple, handles everything automatically.
- Price only (
getSwapPrice): For displaying exchange rates, building price calculators, or checking liquidity without executing. Suitable when frequent price updates are needed - although the data may be slightly less precise.
- Create then execute (
account.quoteSwap() / smartAccount.quoteSwap()): When you need to inspect swap details, implement custom logic, or handle complex scenarios before execution. Note: May reserve funds on-chain and is more strictly rate-limited.
Key differences between Regular Accounts (EOAs) and Smart Accounts:
- Regular accounts (EOAs) return
transactionHash and execute immediately on-chain
- Smart accounts return
userOpHash and execute via user operations with optional gas sponsorship through paymasters
- Smart accounts require an owner account for signing operations
- Smart accounts support batch operations and advanced account abstraction features
All approaches handle Permit2 signatures automatically for ERC20 token swaps. Make sure tokens have proper allowances set for the Permit2 contract before swapping.
Example implementations
To help you get started with token swaps in your application, we provide the following fully-working examples demonstrating different scenarios:
Regular account (EOA) swap examples:
Smart account swap examples:
BYO wallet (viem) regular account (EOA) swap examples:
BYO wallet (viem + account abstraction) smart account swap examples:
Note: The viem smart account examples require additional dependencies (permissionless package) and external service setup (bundler, optional paymaster). For simpler smart account usage, consider CDP's built-in smart account features instead.
Transferring tokens
EVM
For complete examples, check out evm/account.transfer.ts and evm/smartAccount.transfer.ts.
You can transfer tokens between accounts using the transfer function:
const sender = await cdp.evm.createAccount({ name: "Sender" });
const { transactionHash } = await sender.transfer({
to: "0x9F663335Cd6Ad02a37B633602E98866CF944124d",
amount: 10000n,
token: "usdc",
network: "base-sepolia",
});
You can then wait for the transaction receipt with a viem Public Client:
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: transactionHash });
Smart Accounts also have a transfer function:
const sender = await cdp.evm.createSmartAccount({
owner: privateKeyToAccount(generatePrivateKey()),
});
console.log("Created smart account", sender);
const { userOpHash } = await sender.transfer({
to: "0x9F663335Cd6Ad02a37B633602E98866CF944124d",
amount: 10000n,
token: "usdc",
network: "base-sepolia",
});
One difference is that the transfer function returns the user operation hash, which is different from the transaction hash. You can use the returned user operation hash in a call to waitForUserOperation to get the result of the transaction:
const receipt = await sender.waitForUserOperation({
hash: userOpHash,
});
if (receipt.status === "complete") {
console.log(
`Transfer successful! Explorer link: https://sepolia.basescan.org/tx/${receipt.userOpHash}`,
);
} else {
console.log(`Something went wrong! User operation hash: ${receipt.userOpHash}`);
}
Using Smart Accounts, you can also specify a paymaster URL:
await sender.transfer({
to: "0x9F663335Cd6Ad02a37B633602E98866CF944124d",
amount: "0.01",
token: "usdc",
network: "base-sepolia",
paymasterUrl: "https://some-paymaster-url.com",
});
Transfer amount must be passed as a bigint. To convert common tokens from whole units, you can use utilities such as parseEther and parseUnits from viem.
await sender.transfer({
to: "0x9F663335Cd6Ad02a37B633602E98866CF944124d",
amount: parseUnits("0.01", 6),
token: "usdc",
network: "base-sepolia",
});
You can pass usdc or eth as the token to transfer, or you can pass a contract address directly:
await sender.transfer({
to: "0x9F663335Cd6Ad02a37B633602E98866CF944124d",
amount: parseUnits("0.000001", 18),
token: "0x4200000000000000000000000000000000000006",
network: "base-sepolia",
});
You can also pass another account as the to parameter:
const sender = await cdp.evm.createAccount({ name: "Sender" });
const receiver = await cdp.evm.createAccount({ name: "Receiver" });
await sender.transfer({
to: receiver,
amount: 10000n,
token: "usdc",
network: "base-sepolia",
});
Solana
For complete examples, check out solana/account.transfer.ts.
You can transfer tokens between accounts using the transfer function:
import { createSolanaRpc, type Signature } from "@solana/kit";
const sender = await cdp.solana.createAccount();
const { signature } = await sender.transfer({
to: "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE",
amount: 10_000_000n,
token: "sol",
network: "devnet",
});
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const result = await rpc.getSignatureStatuses([signature as Signature]).send();
const status = result.value[0];
if (status?.err) {
console.log(`Something went wrong! Error: ${JSON.stringify(status.err)}`);
} else {
console.log(
`Transaction confirmed: Link: https://explorer.solana.com/tx/${signature}?cluster=devnet`,
);
}
You can also easily send USDC:
const { signature } = await sender.transfer({
to: "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE",
amount: "0.01",
token: "usdc",
network: "devnet",
});
If you want to use your own RPC client, you can pass one to the network parameter:
import { createSolanaRpc } from "@solana/kit";
const rpc = createSolanaRpc("YOUR_RPC_URL");
const { signature } = await sender.transfer({
to: "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE",
amount: "0.01",
token: "usdc",
network: rpc,
});
Account Actions
Account objects have actions that can be used to interact with the account. These can be used in place of the cdp client.
EVM account actions
Here are some examples for actions on EVM accounts.
For example, instead of:
const balances = await cdp.evm.listTokenBalances({
address: account.address,
network: "base-sepolia",
});
You can use the listTokenBalances action:
const account = await cdp.evm.createAccount();
const balances = await account.listTokenBalances({ network: "base-sepolia" });
EvmAccount supports the following actions:
listTokenBalances
requestFaucet
signTransaction
sendTransaction
transfer
EvmSmartAccount supports the following actions:
listTokenBalances
requestFaucet
sendUserOperation
waitForUserOperation
getUserOperation
transfer
Solana account actions
Here are some examples for actions on Solana accounts.
const balances = await cdp.solana.signMessage({
address: account.address,
message: "Hello, world!",
});
You can use the signMessage action:
const account = await cdp.solana.createAccount();
const { signature } = await account.signMessage({
message: "Hello, world!",
});
SolanaAccount supports the following actions:
requestFaucet
signMessage
signTransaction
Policy Management
You can use the policies SDK to manage sets of rules that govern the behavior of accounts and projects, such as enforce allowlists and denylists.
Create a Project-level policy that applies to all accounts
This policy will accept any account sending less than a specific amount of ETH to a specific address.
const policy = await cdp.policies.createPolicy({
policy: {
scope: "project",
description: "Project-wide Allowlist Policy",
rules: [
{
action: "accept",
operation: "signEvmTransaction",
criteria: [
{
type: "ethValue",
ethValue: "1000000000000000000",
operator: "<=",
},
{
type: "evmAddress",
addresses: ["0x000000000000000000000000000000000000dEaD"],
operator: "in",
},
],
},
],
},
});
Create an Account-level policy
This policy will accept any transaction with a value less than or equal to 1 ETH to a specific address.
const policy = await cdp.policies.createPolicy({
policy: {
scope: "account",
description: "Account Allowlist Policy",
rules: [
{
action: "accept",
operation: "signEvmTransaction",
criteria: [
{
type: "ethValue",
ethValue: "1000000000000000000",
operator: "<=",
},
{
type: "evmAddress",
addresses: ["0x000000000000000000000000000000000000dEaD"],
operator: "in",
},
],
},
],
},
});
Create a Solana Allowlist Policy
const policy = await cdp.policies.createPolicy({
policy: {
scope: "account",
description: "Account Allowlist Policy",
rules: [
{
action: "accept",
operation: "signSolTransaction",
criteria: [
{
type: "solAddress",
addresses: ["DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF"],
operator: "in",
},
],
},
],
},
});
List Policies
You can filter by account:
const policy = await cdp.policies.listPolicies({
scope: "account",
});
You can also filter by project:
const policy = await cdp.policies.listPolicies({
scope: "project",
});
Retrieve a Policy
const policy = await cdp.policies.getPolicyById({
id: "__POLICY_ID__",
});
Update a Policy
This policy will update an existing policy to accept transactions to any address except one.
const policy = await cdp.policies.updatePolicy({
id: "__POLICY_ID__",
policy: {
description: "Updated Account Denylist Policy",
rules: [
{
action: "accept",
operation: "signEvmTransaction",
criteria: [
{
type: "evmAddress",
addresses: ["0x000000000000000000000000000000000000dEaD"],
operator: "not in",
},
],
},
],
},
});
Delete a Policy
[!WARNING] Attempting to delete an account-level policy in-use by at least one account will fail.
const policy = await cdp.policies.deletePolicy({
id: "__POLICY_ID__",
});
Validate a Policy
If you're integrating policy editing into your application, you may find it useful to validate policies ahead of time to provide a user with feedback. The CreatePolicyBodySchema and UpdatePolicyBodySchema can be used to get actionable structured information about any issues with a policy. Read more about handling ZodErrors.
import { CreatePolicyBodySchema, UpdatePolicyBodySchema } from "@coinbase/cdp-sdk";
try {
CreatePolicyBodySchema.parse({
description: "Bad description with !#@ characters, also is wayyyyy toooooo long!!",
rules: [
{
action: "acept",
operation: "unknownOperation",
criteria: [
{
type: "ethValue",
ethValue: "not a number",
operator: "<=",
},
{
type: "evmAddress",
addresses: ["not an address"],
operator: "in",
},
{
type: "evmAddress",
addresses: ["not an address"],
operator: "invalid operator",
},
],
},
],
});
} catch (e) {
console.error(e);
}
Supported Policy Rules
We currently support the following policy rules:
Server wallet rules:
End user rules:
SignEndUserEvmTransactionRule — operation: signEndUserEvmTransaction (criteria: ethValue, evmAddress, evmData, netUSDChange)
SendEndUserEvmTransactionRule — operation: sendEndUserEvmTransaction (criteria: ethValue, evmAddress, evmNetwork, evmData, netUSDChange)
SignEndUserEvmMessageRule — operation: signEndUserEvmMessage (criteria: evmMessage)
SignEndUserEvmTypedDataRule — operation: signEndUserEvmTypedData (criteria: evmTypedDataField, evmTypedDataVerifyingContract)
SignEndUserSolTransactionRule — operation: signEndUserSolTransaction (criteria: solAddress, solValue, splAddress, splValue, mintAddress, solData, programId)
SendEndUserSolTransactionRule — operation: sendEndUserSolTransaction (criteria: solAddress, solValue, splAddress, splValue, mintAddress, solData, programId, solNetwork)
SignEndUserSolMessageRule — operation: signEndUserSolMessage (criteria: solMessage)
End user rules use the same criteria types as their server wallet counterparts. For example, signEndUserEvmTransaction supports the same ethValue, evmAddress, and evmData criteria as signEvmTransaction.
End User Policies
You can create policies that govern end-user operations using the same criteria types available for server wallet policies. The only difference is the operation value, which targets end-user-specific actions.
End User EVM Policy
This policy restricts end-user EVM transaction signing to a max value and allowlisted recipients — the same criteria used in signEvmTransaction:
const policy = await cdp.policies.createPolicy({
policy: {
scope: "project",
description: "End User EVM Policy",
rules: [
{
action: "accept",
operation: "signEndUserEvmTransaction",
criteria: [
{
type: "ethValue",
ethValue: "1000000000000000000",
operator: "<=",
},
{
type: "evmAddress",
addresses: ["0x000000000000000000000000000000000000dEaD"],
operator: "in",
},
],
},
],
},
});
End User Solana Policy
This policy restricts end-user Solana transaction signing to allowlisted recipients under a SOL value threshold — the same criteria used in signSolTransaction:
const policy = await cdp.policies.createPolicy({
policy: {
scope: "project",
description: "End User Solana Policy",
rules: [
{
action: "accept",
operation: "signEndUserSolTransaction",
criteria: [
{
type: "solAddress",
addresses: ["11111111111111111111111111111111"],
operator: "in",
},
{
type: "solValue",
solValue: "1000000000",
operator: "<=",
},
],
},
],
},
});
For a comprehensive example demonstrating all 7 end-user operations, see createEndUserPolicy.ts.
End-user Management
You can use the End User SDK to manage the users of your applications.
Create End User
You can create an end user with authentication methods and optionally create EVM and Solana accounts for them.
const endUser = await cdp.endUser.createEndUser({
authenticationMethods: [{ type: "email", email: "user@example.com" }],
evmAccount: { createSmartAccount: true },
solanaAccount: { createSmartAccount: false },
});
console.log(endUser);
Import End User
You can import an existing private key for an end user:
const endUser = await cdp.endUser.importEndUser({
authenticationMethods: [{ type: "email", email: "user@example.com" }],
privateKey: "0x...",
keyType: "evm",
});
console.log(endUser);
You can also import a Solana private key:
const endUser = await cdp.endUser.importEndUser({
authenticationMethods: [{ type: "email", email: "user@example.com" }],
privateKey: "3Kzj...",
keyType: "solana",
});
console.log(endUser);
Add EVM Account to End User
Add an additional EVM EOA (Externally Owned Account) to an existing end user. You can call the method directly on the EndUser object:
const result = await endUser.addEvmAccount();
console.log(`Added EVM account: ${result.evmAccount.address}`);
const result = await cdp.endUser.addEndUserEvmAccount({
userId: endUser.userId,
});
console.log(`Added EVM account: ${result.evmAccount.address}`);
Add EVM Smart Account to End User
Add an EVM smart account to an existing end user:
const result = await endUser.addEvmSmartAccount({ enableSpendPermissions: true });
console.log(`Added EVM smart account: ${result.evmSmartAccount.address}`);
const result = await cdp.endUser.addEndUserEvmSmartAccount({
userId: endUser.userId,
enableSpendPermissions: true,
});
console.log(`Added EVM smart account: ${result.evmSmartAccount.address}`);
Add Solana Account to End User
Add an additional Solana account to an existing end user:
const result = await endUser.addSolanaAccount();
console.log(`Added Solana account: ${result.solanaAccount.address}`);
const result = await cdp.endUser.addEndUserSolanaAccount({
userId: endUser.userId,
});
console.log(`Added Solana account: ${result.solanaAccount.address}`);
Validate Access Token
When your end user has signed in with an Embedded Wallet, you can check whether the access token they were granted is valid, and which of your user's it is associated with.
try {
const endUser = await cdp.endUser.validateAccessToken({
accessToken,
});
console.log(endUser);
} catch (e) {
}
Delegated Signing Operations
When an end user has granted a delegation, you can sign and send transactions on their behalf using the cdp.endUser client methods or directly on the EndUserAccount object.
All delegated operations are available both as client methods (passing userId explicitly) and as convenience methods on the EndUserAccount object (where userId is automatically bound and address defaults to the first account if not specified).
Revoke Delegation
Revoke all active delegations for an end user:
await cdp.endUser.revokeDelegationForEndUser({
userId: "user-123",
});
const endUser = await cdp.endUser.getEndUser({ userId: "user-123" });
await endUser.revokeDelegation();
For a complete example, see end-users/revokeDelegation.ts.
EVM Signing
Sign an EVM Transaction
const result = await cdp.endUser.signEvmTransaction({
userId: "user-123",
address: "0x1234...",
transaction: "0x02...",
});
console.log(result.signedTransaction);
const result = await endUser.signEvmTransaction({
transaction: "0x02...",
});
console.log(result.signedTransaction);
Sign an EVM Message (EIP-191)
const result = await cdp.endUser.signEvmMessage({
userId: "user-123",
address: "0x1234...",
message: "Hello, World!",
});
console.log(result.signature);
const result = await endUser.signEvmMessage({
message: "Hello, World!",
});
console.log(result.signature);
Sign EVM Typed Data (EIP-712)
const result = await cdp.endUser.signEvmTypedData({
userId: "user-123",
address: "0x1234...",
typedData: {
domain: { name: "Example" },
types: { Message: [{ name: "content", type: "string" }] },
primaryType: "Message",
message: { content: "Hello" },
},
});
console.log(result.signature);
const result = await endUser.signEvmTypedData({
typedData: {
domain: { name: "Example" },
types: { Message: [{ name: "content", type: "string" }] },
primaryType: "Message",
message: { content: "Hello" },
},
});
console.log(result.signature);
EVM Sending
Send an EVM Transaction
const result = await cdp.endUser.sendEvmTransaction({
userId: "user-123",
address: "0x1234...",
transaction: "0x02...",
network: "base-sepolia",
});
console.log(result.transactionHash);
const result = await endUser.sendEvmTransaction({
transaction: "0x02...",
network: "base-sepolia",
});
console.log(result.transactionHash);
For a complete example, see end-users/sendEvmTransaction.ts.
Send an EVM Asset
Send tokens (e.g. USDC) on behalf of an end user:
const result = await cdp.endUser.sendEvmAsset({
userId: "user-123",
address: "0x1234...",
to: "0xabcd...",
amount: "1000000",
network: "base-sepolia",
asset: "usdc",
useCdpPaymaster: true,
});
console.log(result.transactionHash);
const result = await endUser.sendEvmAsset({
to: "0xabcd...",
amount: "1000000",
network: "base-sepolia",
});
console.log(result.transactionHash);
For a complete example, see end-users/sendEvmAsset.ts.
Send a User Operation (Smart Account)
Send a user operation via an end user's smart account:
const result = await cdp.endUser.sendUserOperation({
userId: "user-123",
address: "0x1234...",
network: "base-sepolia",
calls: [
{
to: "0xabcd...",
value: "0",
data: "0x",
},
],
useCdpPaymaster: true,
});
const result = await endUser.sendUserOperation({
network: "base-sepolia",
calls: [
{
to: "0xabcd...",
value: "0",
data: "0x",
},
],
useCdpPaymaster: true,
});
For a complete example, see end-users/sendUserOperation.ts.
EVM EIP-7702 Delegation
Create an EIP-7702 delegation on behalf of an end user, upgrading their EOA with smart account capabilities:
const result = await cdp.endUser.createEvmEip7702Delegation({
userId: "user-123",
address: "0x1234...",
network: "base-sepolia",
enableSpendPermissions: false,
});
console.log(result.delegationOperationId);
const result = await endUser.createEvmEip7702Delegation({
network: "base-sepolia",
});
console.log(result.delegationOperationId);
For a complete example, see end-users/createEvmEip7702Delegation.ts.
Solana Signing
Sign a Solana Message
const result = await cdp.endUser.signSolanaMessage({
userId: "user-123",
address: "So1ana...",
message: "base64message...",
});
console.log(result.signature);
const result = await endUser.signSolanaMessage({
message: "base64message...",
});
console.log(result.signature);
For a complete example, see end-users/signSolanaMessage.ts.
Sign a Solana Transaction
const result = await cdp.endUser.signSolanaTransaction({
userId: "user-123",
address: "So1ana...",
transaction: "base64tx...",
});
console.log(result.signedTransaction);
const result = await endUser.signSolanaTransaction({
transaction: "base64tx...",
});
console.log(result.signedTransaction);
Solana Sending
Send a Solana Transaction
const result = await cdp.endUser.sendSolanaTransaction({
userId: "user-123",
address: "So1ana...",
transaction: "base64tx...",
network: "solana-devnet",
});
console.log(result.transactionSignature);
const result = await endUser.sendSolanaTransaction({
transaction: "base64tx...",
network: "solana-devnet",
});
console.log(result.transactionSignature);
Send a Solana Asset
const result = await cdp.endUser.sendSolanaAsset({
userId: "user-123",
address: "So1ana...",
to: "Recipi...",
amount: "1000000",
network: "solana-devnet",
asset: "usdc",
createRecipientAta: true,
});
console.log(result.transactionSignature);
const result = await endUser.sendSolanaAsset({
to: "Recipi...",
amount: "1000000",
network: "solana-devnet",
});
console.log(result.transactionSignature);
x402 Payment Protocol
x402 is an open payment protocol that lets clients pay for HTTP requests using the 402 Payment Required status code. The SDK ships x402 support in the @coinbase/cdp-sdk/x402 subpath, which builds on top of the @x402 packages:
- Payment client (
CdpX402Client) — pay for x402-protected APIs with a CDP-managed wallet
- Spend controls — SDK-managed guardrails (per-payment caps, cumulative caps, allowlists) for autonomous agents
- Resource server (
createX402Server) — add x402 payment gating to your HTTP endpoints
- Facilitator (
createCdpFacilitatorClient) — the CDP-hosted payment facilitator for verifying and settling payments
- Signer adapters (
fromCdpEvmAccount, fromCdpSmartWallet, cdpSolanaAccountToSvmSigner) — bridge CDP accounts into an existing @x402 setup
- Direct signing (
signX402Payment) — sign a payment payload with a CDP account without an HTTP round-trip
All entry points resolve CDP_API_KEY_ID, CDP_API_KEY_SECRET, and (where a wallet is required) CDP_WALLET_SECRET from environment variables, and accept explicit overrides via config. See the x402 examples for complete, runnable programs.
@x402/core, @x402/evm, @x402/extensions, and @x402/svm are optional peer dependencies — install the ones you need alongside @coinbase/cdp-sdk (e.g. npm install @x402/core @x402/evm @x402/svm) rather than expecting them to come along automatically. @x402/fetch isn't used by the SDK itself, but pairs well with CdpX402Client if you want its wrapFetchWithPayment helper — install it separately if so.
Pay for an x402-protected API
CdpX402Client extends @x402/core's x402Client and auto-provisions a CDP-managed wallet on the first payment. Pass it to wrapFetchWithPayment from @x402/fetch to transparently pay for 402-gated requests.
import { CdpX402Client } from "@coinbase/cdp-sdk/x402";
import { wrapFetchWithPayment } from "@x402/fetch";
const client = new CdpX402Client();
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("https://api.example.com/report");
The wallet used for payment is managed internally (a CDP Server Wallet named "x402-client-wallet-1" by default). To find its address — for example, to fund it before paying — call getAddresses(), which provisions the wallet eagerly instead of waiting for the first payment:
const client = new CdpX402Client();
const { evmAddress, svmAddress } = await client.getAddresses();
console.log("Fund this address before paying:", evmAddress, svmAddress);
By default the client uses a CDP Server Wallet (EOA). To pay from a CDP Smart Account instead, supply a walletConfig:
const client = new CdpX402Client({
walletConfig: {
type: "smart",
accountName: "my-smart-wallet",
ownerAccountName: "my-owner",
},
});
Apply spend controls
Attach spendControls to CdpX402Client to enforce per-payment and cumulative caps, restrict networks/assets/payees, and receive callbacks as spend approaches a limit. A blocked payment throws a SpendControlError with a machine-readable code.
import { CdpX402Client, SpendControlError } from "@coinbase/cdp-sdk/x402";
const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const client = new CdpX402Client({
spendControls: {
maxAmountPerPayment: { atomic: 10_000n, asset: USDC_BASE },
maxCumulativeSpend: { atomic: 50_000n, asset: USDC_BASE },
maxCumulativeSpendWindow: "24h",
allowedNetworks: ["eip155:8453"],
onApproachingLimit: (spent, limit) => {
const pct = (Number(spent.atomic) / Number(limit.atomic)) * 100;
console.warn(`Approaching spend limit: ${pct.toFixed(0)}% of cap used`);
},
},
});
Spend controls can also be applied to any @x402/core client directly via applySpendControls(client, controls). Provide a persistent SpendStore (via the store option) if you need the cumulative ledger to survive process restarts — the default store is in-memory.
Gate an HTTP endpoint
createX402Server provisions a receiver wallet and returns a server descriptor you can hand to an @x402 HTTP middleware (for example @x402/express).
import { createX402Server } from "@coinbase/cdp-sdk/x402";
import { paymentMiddlewareFromHTTPServer } from "@x402/express";
const server = await createX402Server({
routes: { "GET /report": { price: "$0.01", description: "AI-generated report" } },
});
app.use(paymentMiddlewareFromHTTPServer(server));
console.log("Receiving EVM payments at", server.payToEvmAddress);
Use the CDP-hosted facilitator
createCdpFacilitatorClient returns a CDP-authenticated HTTPFacilitatorClient that verifies and settles payments through the CDP-hosted facilitator. It is a drop-in replacement for a self-hosted facilitator and only needs API-key credentials (no wallet secret).
import { createCdpFacilitatorClient } from "@coinbase/cdp-sdk/x402";
import { x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";
const facilitator = createCdpFacilitatorClient();
const server = new x402ResourceServer(facilitator).register("eip155:8453", new ExactEvmScheme());
Use a CDP wallet with an existing x402 client
If you already have an @x402 client set up, use the signer adapters to sign with a CDP-managed account instead of a local private key.
import { CdpClient } from "@coinbase/cdp-sdk";
import { fromCdpEvmAccount } from "@coinbase/cdp-sdk/x402";
import { x402Client } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
const cdp = new CdpClient();
const account = await cdp.evm.getOrCreateAccount({ name: "my-signer" });
const client = new x402Client();
registerExactEvmScheme(client, { signer: fromCdpEvmAccount(account) });
Use fromCdpSmartWallet for a CDP Smart Account and cdpSolanaAccountToSvmSigner for a CDP Solana account.
Sign an x402 payment payload directly
CDP accounts can also sign x402 payment payloads directly. Direct signing is useful when the payment payload needs to travel over non-HTTP transports such as gRPC metadata, MCP tool results, queues, or batch jobs.
Use signX402Payment(paymentRequired, acceptedIndex) on any CDP-managed EVM account, EVM smart account, or Solana account. paymentRequired is the x402 payment requirement object returned by a resource server. acceptedIndex selects which entry in paymentRequired.accepts to sign.
import { CdpClient } from "@coinbase/cdp-sdk";
const cdp = new CdpClient();
const account = await cdp.evm.getOrCreateAccount({ name: "agent" });
const acceptedIndex = 0;
const payment = await account.signX402Payment(paymentRequired, acceptedIndex);
metadata.set("x402-payment", Buffer.from(JSON.stringify(payment)).toString("base64"));
return {
content: [{ type: "text", text: "paid result" }],
_meta: { x402Payment: payment },
};
await queue.publish("paid-job", { payment, jobId });
For smart accounts (supports the EIP-3009 exact flow only — smart accounts sign with an ERC-1271/ERC-6492 contract signature, so the Permit2-based upto scheme and exact requirements that use the Permit2 transfer method are not supported):
const owner = await cdp.evm.getOrCreateAccount({ name: "agent-owner" });
const smartAccount = await cdp.evm.getOrCreateSmartAccount({
name: "agent-smart-account",
owner,
});
const acceptedIndex = 0;
const payment = await smartAccount.signX402Payment(paymentRequired, acceptedIndex);
For Solana accounts, the same surface produces an exact SVM payment payload:
const solanaAccount = await cdp.solana.getOrCreateAccount({ name: "solana-agent" });
const acceptedIndex = 0;
const payment = await solanaAccount.signX402Payment(paymentRequired, acceptedIndex);
Webhooks
You can use the webhooks SDK to subscribe to on-chain and wallet events and receive notifications at a URL of your choice.
Create Subscription
Create a webhook subscription to receive event notifications:
const subscription = await cdp.webhooks.createSubscription({
description: "Monitor wallet transactions",
eventTypes: [
"wallet.transaction.pending",
"wallet.transaction.confirmed",
"wallet.transaction.failed",
],
targetUrl: "https://example.com/webhook",
targetHeaders: { "X-Custom-Header": "custom-value" },
isEnabled: true,
metadata: { env: "production" },
});
console.log("Subscription ID:", subscription.subscriptionId);
console.log("Secret:", subscription.secret);
The available wallet event types are:
wallet.transaction.created
wallet.transaction.broadcast
wallet.transaction.pending
wallet.transaction.replaced
wallet.transaction.confirmed
wallet.transaction.failed
wallet.transaction.signed
wallet.typed_data.signed
wallet.message.signed
wallet.hash.signed
wallet.delegation.created
wallet.delegation.revoked
For a complete working example, see webhooks/createWebhookSubscription.ts.
Authentication tools
This SDK also contains simple tools for authenticating REST API requests to the Coinbase Developer Platform (CDP). See the Auth README for more details.
Error Reporting
This SDK contains error reporting functionality that sends error events to CDP. If you would like to disable this behavior, you can set the DISABLE_CDP_ERROR_REPORTING environment variable to true.
DISABLE_CDP_ERROR_REPORTING=true
Usage Tracking
This SDK contains usage tracking functionality that sends usage events to CDP. If you would like to disable this behavior, you can set the DISABLE_CDP_USAGE_TRACKING environment variable to true.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For feature requests, feedback, or questions, please reach out to us in the #cdp-sdk channel of the Coinbase Developer Platform Discord.
Security
If you discover a security vulnerability within this SDK, please see our Security Policy for disclosure information.
FAQ
Common errors and their solutions.
TypeScript compilation errors with generateJwt or moduleResolution
If you encounter TypeScript compilation errors when using the CDP SDK, particularly with generateJwt or import statements, you may need to update your TypeScript configuration.
Error symptoms:
- Type errors with
generateJwt function
- Module resolution errors
- Import/export type mismatches
Solution:
Update your tsconfig.json to use a modern module resolution strategy. Change moduleResolution from node to node16 or nodenext:
{
"compilerOptions": {
"moduleResolution": "node16"
}
}
The CDP SDK is built as an ESM package and moduleResolution: "node16" or "nodenext" should be used for proper type resolution. The legacy "node" setting doesn't correctly resolve ESM package exports.
AggregateError [ETIMEDOUT]
This is an issue in Node.js itself: https://github.com/nodejs/node/issues/54359. While the fix is implemented, the workaround is to set the environment variable:
export NODE_OPTIONS="--network-family-autoselection-attempt-timeout=500"
Error [ERR_REQUIRE_ESM]: require() of ES modules is not supported.
Use Node v20.19.0 or higher. CDP SDK depends on jose v6, which ships only ESM. Jose supports CJS style imports in Node.js versions where the require(esm) feature is enabled by default (^20.19.0 || ^22.12.0 || >= 23.0.0). See here for more info.
Jest encountered an unexpected token
If you're using Jest and see an error like this:
Details:
/Users/.../node_modules/jose/dist/webapi/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { compactDecrypt } from './jwe/compact/decrypt.js';
^^^^^^
SyntaxError: Unexpected token 'export'
Add a file called jest.setup.ts next to your jest.config file with the following content:
jest.mock("jose", () => {});
Then, add the following line to your jest.config file:
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],