Documentation

Coinbase Developer Platform (CDP) TypeScript SDK

Tip

If you're looking to contribute to the SDK, please see the Contributing Guide.

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.

CDP SDK has auto-generated docs for the Typescript SDK.

Further documentation is also available on the CDP docs website:

npm install @coinbase/cdp-sdk

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.

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

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

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",
});
const account = await cdp.evm.createAccount();
const account = await cdp.evm.importAccount({
privateKey: "0x123456",
name: "MyAccount",
});
const account = await cdp.solana.createAccount();
const account = await cdp.evm.getOrCreateAccount({
name: "Account1",
});
const account = await cdp.solana.getOrCreateAccount({
name: "Account1",
});
const account = await cdp.evm.updateAccount({
addresss: account.address,
update: {
name: "Updated name",
accountPolicy: "1622d4b7-9d60-44a2-9a6a-e9bbb167e412",
},
});
const account = await cdp.solana.updateAccount({
addresss: account.address,
update: {
name: "Updated name",
accountPolicy: "1622d4b7-9d60-44a2-9a6a-e9bbb167e412",
},
});

You can use the faucet function to request testnet ETH or SOL from the CDP.

const faucetResp = await cdp.evm.requestFaucet({
address: evmAccount.address,
network: "base-sepolia",
token: "eth",
});
const faucetResp = await cdp.solana.requestFaucet({
address: fromAddress,
token: "sol",
});

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(),
});

// Step 3: Sign the transaction with CDP and broadcast it using the wallet client.
const hash = await walletClient.sendTransaction({
to: "0x4252e0c9A3da5A2700e7d91cb50aEf522D0C6Fe8",
value: parseEther("0.000001"),
});

console.log(`Transaction confirmed! Explorer link: https://sepolia.basescan.org/tx/${hash}`);

For Solana, we recommend using the @solana/web3.js library to send transactions. See the examples.

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.

const evmAccount = await cdp.evm.createAccount();
const smartAccount = await cdp.evm.createSmartAccount({
owner: evmAccount,
});
const userOperation = await cdp.evm.sendUserOperation({
smartAccount: smartAccount,
network: "base-sepolia",
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: parseEther("0.000001"),
data: "0x",
},
],
});
const userOperation = await cdp.sendUserOperation({
smartAccount: smartAccount,
network: "base-sepolia",
calls: [
{
to: "0x0000000000000000000000000000000000000000",
value: parseEther("0"),
data: "0x",
},
],
paymasterUrl: "https://some-paymaster-url.com",
});

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, // equivalent to 0.01 USDC
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, // equivalent to 0.01 USDC
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), // USDC has 6 decimals
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), // WETH has 18 decimals. equivalent to calling `parseEther("0.000001")`
token: "0x4200000000000000000000000000000000000006", // WETH on Base Sepolia
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, // equivalent to 0.01 USDC
token: "usdc",
network: "base-sepolia",
});

For complete examples, check out solana/account.transfer.ts.

You can transfer tokens between accounts using the transfer function, and wait for the transaction to be confirmed using the confirmTransaction function from @solana/web3.js:

import { LAMPORTS_PER_SOL } from "@solana/web3.js";

const sender = await cdp.solana.createAccount();

const connection = new Connection("https://api.devnet.solana.com");

const { signature } = await sender.transfer({
to: "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE",
amount: 0.01 * LAMPORTS_PER_SOL,
token: "sol",
network: connection,
});

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();

const confirmation = await connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
},
"confirmed",
);

if (confirmation.value.err) {
console.log(`Something went wrong! Error: ${confirmation.value.err.toString()}`);
} 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 Connection, you can pass one to the network parameter:

import { Connection } from "@solana/web3.js";

const connection = new Connection("YOUR_RPC_URL");

const { signature } = await sender.transfer({
to: "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE",
amount: "0.01",
token: "usdc",
network: connection,
});

Account objects have actions that can be used to interact with the account. These can be used in place of the cdp client.

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

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

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.

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",
},
],
},
],
},
});

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",
},
],
},
],
},
});
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",
},
],
},
],
},
});

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",
});
const policy = await cdp.policies.getPolicyById({
id: "__POLICY_ID__",
});

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",
},
],
},
],
},
});
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__",
});

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";

// Validate a new Policy with many issues, will throw a ZodError with actionable validation errors
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);
}

This SDK also contains simple tools for authenticating REST API requests to the Coinbase Developer Platform (CDP). See the Auth README for more details.

This SDK contains error reporting functionality that sends error events to the 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

This project is licensed under the MIT License - see the LICENSE file for details.

For feature requests, feedback, or questions, please reach out to us in the #cdp-sdk channel of the Coinbase Developer Platform Discord.

If you discover a security vulnerability within this SDK, please see our Security Policy for disclosure information.

Common errors and their solutions.

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"

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.

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"],