<100 subscribers
Automating on-chain actions on Solana can feel risky: one bad key leak or buggy transaction and funds are gone. Traditional guards like code reviews and unit tests help, but they don’t stop a live transaction.
Today, you’ll fix that with Coinbase Developer Platform (CDP) Server Wallets v2. We’ll create policies — API-level firewalls that allow only the transactions you intend. By the end, you’ll have a runnable TypeScript script that enforces SOL/SPL allowlists and a strict IDL-based policy for Jupiter v6 swaps.
🔐 Create policies that only allow SOL transfers to trusted addresses
Restrict SPL token transfers to approved mints (USDC)
🔎 Validate instruction data with a custom IDL (Jupiter v6 “route”)
🚀 Assemble a single script that creates all policies in one go
Dev Script (TypeScript)
│ uses
▼
CDP SDK (Policies API)
│ creates
▼
Policy Objects (scope: account)
│ enforced at
▼
Server Wallets v2 ──> Solana RuntimeCDP SDK — Programmatic entry to create/update policies.
Policies — Declarative rules (allowlists, mint restrictions, IDL checks) that gate transactions before signing.
Server Wallets v2 — Managed, programmatic wallets that must satisfy active policies.
Solana — Actual execution layer; transactions only flow if they pass the policy engine.
You’ll need
Node.js v18+
A CDP account, API Key (id + secret), and Wallet Secret from the CDP Portal
Create & init
# Create your project
mkdir solana-policy-engine-tutorial
cd solana-policy-engine-tutorial
# Initialize Node.js project
npm init -yInstall deps
npm install @coinbase/cdp-sdk dotenv typescript ts-node @types/nodeInitialize TypeScript
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --module commonjs --moduleResolution nodeScaffold files
mkdir src
touch src/main.ts
touch .envAdd secrets to .env
# .env - Get these from the CDP Portal
CDP_API_KEY_ID="your_api_key_id"
CDP_API_KEY_SECRET="your_api_key_secret"
CDP_WALLET_SECRET="your_wallet_secret"Create src/main.ts:
// src/main.ts
import { CdpClient } from "@coinbase/cdp-sdk";
import dotenv from "dotenv";
dotenv.config();
const cdp = new CdpClient();
console.log("CDP Client Initialized Successfully.");
async function main() {
console.log("\n--- Starting Policy Creation Script ---");
// We'll call our policy functions here.
console.log("\n--- ✅ All Policies Created Successfully ---");
}
main().catch((error) => {
console.error("\nAn error occurred during policy creation:", error);
process.exit(1);
});Only allow SOL transfers to a trusted set of addresses.
/**
* Policy 1: SOL Address Allowlist
* This policy only permits sending SOL to a specific list of trusted addresses.
* Any attempt to send SOL elsewhere will be blocked at the API level.
*/
async function createSolAllowlistPolicy(cdp: CdpClient) {
console.log("\nCreating a SOL address allowlist policy...");
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow SOL transfers to treasury addresses",
scope: "account", // This policy will be applied to a specific account
rules: [
{
action: "accept", // Required: accept or deny
operation: "signSolTransaction", // Required: Solana transaction operation
criteria: [
{
type: "solAddress",
addresses: [
"DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF", // Valid Treasury Address 1
"HpabPRRCFbBKSuJr5PdkVvQc85FyxyTWkFM2obBRSvHT", // Valid Treasury Address 2
],
operator: "in" // The 'to' address must be IN this list
}
]
}
]
}
});
console.log(`Successfully created SOL Allowlist Policy with ID: ${policy.id}`);
return policy.id;
}Permit transfers only for approved mints (USDC shown below).
/**
* Policy 2: SPL Token Mint Restriction
* This policy only allows transfers of specific SPL tokens (USDC in this case)
* by checking the token's mint address.
*/
async function createStablecoinOnlyPolicy(cdp: CdpClient) {
console.log("\nCreating a stablecoin-only (USDC) transfer policy...");
const USDC_MINT_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow USDC SPL Token transfers only",
scope: "account",
rules: [
{
action: "accept",
operation: "signSolTransaction",
criteria: [
{
type: "mintAddress",
addresses: [USDC_MINT_ADDRESS],
operator: "in"
}
]
}
]
}
});
console.log(`Successfully created Stablecoin-Only Policy with ID: ${policy.id}`);
return policy.id;
}Validate instruction data using the program’s IDL so only intended Jupiter “route” swaps are allowed — and cap the size.
Save the Jupiter v6 IDL JSON as src/jupiter-v6-idl.json (from explorer/IDL source).
Add the policy creator:
/**
* Policy 3: Advanced Jupiter Swap Policy (solData + minimal IDL)
* - Enforces a maximum input amount for Jupiter `route` swaps
* - Validates instruction data against a minimal inline IDL built from
* `src/jupiter-v6-idl.json`
* - Effect: Only allows signing Jupiter `route` swaps when `in_amount` <= 1 SOL;
* other transactions won't satisfy this rule
*/
async function createJupiterSwapPolicy(cdp: CdpClient) {
console.log("\nCreating an advanced policy for Jupiter swaps...");
const MAX_SWAP_AMOUNT_LAMPORTS = "1000000000"; // 1 SOL (lamports, as string)
const JUPITER_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
// Build a minimal inline IDL for the `route` instruction for the API
const routeIx = (jupiterIdl.instructions as any[]).find((ix) => ix?.name === 'route');
if (!routeIx) {
throw new Error('Jupiter IDL does not include route instruction');
}
const jupiterRouteIdl = {
address: JUPITER_PROGRAM_ID,
instructions: [
{
name: 'route',
discriminator: routeIx.discriminator,
args: [
{ name: 'route_plan', type: 'vec<defined:RoutePlanStep>' },
{ name: 'in_amount', type: 'u64' },
{ name: 'quoted_out_amount', type: 'u64' },
{ name: 'slippage_bps', type: 'u16' },
{ name: 'platform_fee_bps', type: 'u8' },
],
},
],
};
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow SOL USDC swaps on Jupiter max 1 SOL",
scope: "account",
rules: [
{
action: "accept",
operation: "signSolTransaction",
criteria: [
{
type: "solData",
idls: [jupiterRouteIdl],
conditions: [
{
instruction: "route",
// Guardrail: limit maximum input amount for swaps
params: [
{ name: "in_amount", operator: "<=", value: MAX_SWAP_AMOUNT_LAMPORTS }
]
}
]
}
]
}
]
}
});
console.log(`Successfully created Jupiter Swap Policy with ID: ${policy.id}`);
return policy.id;
}Update the bottom of src/main.ts to create all policies:
/**
* Main: create three policies in sequence
*/
async function main() {
console.log("\n--- Starting Policy Creation Script ---");
try {
// Create foundational policies
await createSolAllowlistPolicy(cdp);
await createStablecoinOnlyPolicy(cdp);
// Create advanced, IDL-based policy
await createJupiterSwapPolicy(cdp);
console.log("\n--- ✅ All Policies Created Successfully ---");
} catch (error) {
console.error("\nAn error occurred during policy creation:", error);
throw error;
}
}
// Execute the main function with proper error handling
main().catch(error => {
console.error("\nFatal error in policy creation:", error);
process.exit(1);
});Run it
npx tsc && node dist/main.jsWatch the console for policy IDs. These policies now protect your account-scoped server wallet operations.
You just stood up a practical Solana policy layer: SOL allowlists, SPL mint restrictions, and a Jupiter v6 IDL policy that inspects instruction data and caps swap size. This is the backbone of safer backends — especially for bots, treasuries, and automated services.
Expand the allowlists/mints for your app
Add program-specific IDL rules for other protocols
Combine with alerting & CI to treat policies as code
Coinbase Developer Docs: https://docs.cdp.coinbase.com
Join the Coinbase Developer Discord to swap patterns and ask questions
Jupiter — (find program/IDL references via explorers)
CDP Wallets v2 Docs:
CDP SDK Github:
Demo Github:
HeimLabs:
Clap if this saved you hours, and share what you’re building with the CDP stack!
Follow HeimLabs for unapologetically practical Web3 dev content.
Twitter, LinkedIn.
Happy Building 🚀
Automating on-chain actions on Solana can feel risky: one bad key leak or buggy transaction and funds are gone. Traditional guards like code reviews and unit tests help, but they don’t stop a live transaction.
Today, you’ll fix that with Coinbase Developer Platform (CDP) Server Wallets v2. We’ll create policies — API-level firewalls that allow only the transactions you intend. By the end, you’ll have a runnable TypeScript script that enforces SOL/SPL allowlists and a strict IDL-based policy for Jupiter v6 swaps.
🔐 Create policies that only allow SOL transfers to trusted addresses
Restrict SPL token transfers to approved mints (USDC)
🔎 Validate instruction data with a custom IDL (Jupiter v6 “route”)
🚀 Assemble a single script that creates all policies in one go
Dev Script (TypeScript)
│ uses
▼
CDP SDK (Policies API)
│ creates
▼
Policy Objects (scope: account)
│ enforced at
▼
Server Wallets v2 ──> Solana RuntimeCDP SDK — Programmatic entry to create/update policies.
Policies — Declarative rules (allowlists, mint restrictions, IDL checks) that gate transactions before signing.
Server Wallets v2 — Managed, programmatic wallets that must satisfy active policies.
Solana — Actual execution layer; transactions only flow if they pass the policy engine.
You’ll need
Node.js v18+
A CDP account, API Key (id + secret), and Wallet Secret from the CDP Portal
Create & init
# Create your project
mkdir solana-policy-engine-tutorial
cd solana-policy-engine-tutorial
# Initialize Node.js project
npm init -yInstall deps
npm install @coinbase/cdp-sdk dotenv typescript ts-node @types/nodeInitialize TypeScript
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --module commonjs --moduleResolution nodeScaffold files
mkdir src
touch src/main.ts
touch .envAdd secrets to .env
# .env - Get these from the CDP Portal
CDP_API_KEY_ID="your_api_key_id"
CDP_API_KEY_SECRET="your_api_key_secret"
CDP_WALLET_SECRET="your_wallet_secret"Create src/main.ts:
// src/main.ts
import { CdpClient } from "@coinbase/cdp-sdk";
import dotenv from "dotenv";
dotenv.config();
const cdp = new CdpClient();
console.log("CDP Client Initialized Successfully.");
async function main() {
console.log("\n--- Starting Policy Creation Script ---");
// We'll call our policy functions here.
console.log("\n--- ✅ All Policies Created Successfully ---");
}
main().catch((error) => {
console.error("\nAn error occurred during policy creation:", error);
process.exit(1);
});Only allow SOL transfers to a trusted set of addresses.
/**
* Policy 1: SOL Address Allowlist
* This policy only permits sending SOL to a specific list of trusted addresses.
* Any attempt to send SOL elsewhere will be blocked at the API level.
*/
async function createSolAllowlistPolicy(cdp: CdpClient) {
console.log("\nCreating a SOL address allowlist policy...");
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow SOL transfers to treasury addresses",
scope: "account", // This policy will be applied to a specific account
rules: [
{
action: "accept", // Required: accept or deny
operation: "signSolTransaction", // Required: Solana transaction operation
criteria: [
{
type: "solAddress",
addresses: [
"DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF", // Valid Treasury Address 1
"HpabPRRCFbBKSuJr5PdkVvQc85FyxyTWkFM2obBRSvHT", // Valid Treasury Address 2
],
operator: "in" // The 'to' address must be IN this list
}
]
}
]
}
});
console.log(`Successfully created SOL Allowlist Policy with ID: ${policy.id}`);
return policy.id;
}Permit transfers only for approved mints (USDC shown below).
/**
* Policy 2: SPL Token Mint Restriction
* This policy only allows transfers of specific SPL tokens (USDC in this case)
* by checking the token's mint address.
*/
async function createStablecoinOnlyPolicy(cdp: CdpClient) {
console.log("\nCreating a stablecoin-only (USDC) transfer policy...");
const USDC_MINT_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow USDC SPL Token transfers only",
scope: "account",
rules: [
{
action: "accept",
operation: "signSolTransaction",
criteria: [
{
type: "mintAddress",
addresses: [USDC_MINT_ADDRESS],
operator: "in"
}
]
}
]
}
});
console.log(`Successfully created Stablecoin-Only Policy with ID: ${policy.id}`);
return policy.id;
}Validate instruction data using the program’s IDL so only intended Jupiter “route” swaps are allowed — and cap the size.
Save the Jupiter v6 IDL JSON as src/jupiter-v6-idl.json (from explorer/IDL source).
Add the policy creator:
/**
* Policy 3: Advanced Jupiter Swap Policy (solData + minimal IDL)
* - Enforces a maximum input amount for Jupiter `route` swaps
* - Validates instruction data against a minimal inline IDL built from
* `src/jupiter-v6-idl.json`
* - Effect: Only allows signing Jupiter `route` swaps when `in_amount` <= 1 SOL;
* other transactions won't satisfy this rule
*/
async function createJupiterSwapPolicy(cdp: CdpClient) {
console.log("\nCreating an advanced policy for Jupiter swaps...");
const MAX_SWAP_AMOUNT_LAMPORTS = "1000000000"; // 1 SOL (lamports, as string)
const JUPITER_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";
// Build a minimal inline IDL for the `route` instruction for the API
const routeIx = (jupiterIdl.instructions as any[]).find((ix) => ix?.name === 'route');
if (!routeIx) {
throw new Error('Jupiter IDL does not include route instruction');
}
const jupiterRouteIdl = {
address: JUPITER_PROGRAM_ID,
instructions: [
{
name: 'route',
discriminator: routeIx.discriminator,
args: [
{ name: 'route_plan', type: 'vec<defined:RoutePlanStep>' },
{ name: 'in_amount', type: 'u64' },
{ name: 'quoted_out_amount', type: 'u64' },
{ name: 'slippage_bps', type: 'u16' },
{ name: 'platform_fee_bps', type: 'u8' },
],
},
],
};
const policy = await cdp.policies.createPolicy({
policy: {
description: "Allow SOL USDC swaps on Jupiter max 1 SOL",
scope: "account",
rules: [
{
action: "accept",
operation: "signSolTransaction",
criteria: [
{
type: "solData",
idls: [jupiterRouteIdl],
conditions: [
{
instruction: "route",
// Guardrail: limit maximum input amount for swaps
params: [
{ name: "in_amount", operator: "<=", value: MAX_SWAP_AMOUNT_LAMPORTS }
]
}
]
}
]
}
]
}
});
console.log(`Successfully created Jupiter Swap Policy with ID: ${policy.id}`);
return policy.id;
}Update the bottom of src/main.ts to create all policies:
/**
* Main: create three policies in sequence
*/
async function main() {
console.log("\n--- Starting Policy Creation Script ---");
try {
// Create foundational policies
await createSolAllowlistPolicy(cdp);
await createStablecoinOnlyPolicy(cdp);
// Create advanced, IDL-based policy
await createJupiterSwapPolicy(cdp);
console.log("\n--- ✅ All Policies Created Successfully ---");
} catch (error) {
console.error("\nAn error occurred during policy creation:", error);
throw error;
}
}
// Execute the main function with proper error handling
main().catch(error => {
console.error("\nFatal error in policy creation:", error);
process.exit(1);
});Run it
npx tsc && node dist/main.jsWatch the console for policy IDs. These policies now protect your account-scoped server wallet operations.
You just stood up a practical Solana policy layer: SOL allowlists, SPL mint restrictions, and a Jupiter v6 IDL policy that inspects instruction data and caps swap size. This is the backbone of safer backends — especially for bots, treasuries, and automated services.
Expand the allowlists/mints for your app
Add program-specific IDL rules for other protocols
Combine with alerting & CI to treat policies as code
Coinbase Developer Docs: https://docs.cdp.coinbase.com
Join the Coinbase Developer Discord to swap patterns and ask questions
Jupiter — (find program/IDL references via explorers)
CDP Wallets v2 Docs:
CDP SDK Github:
Demo Github:
HeimLabs:
Clap if this saved you hours, and share what you’re building with the CDP stack!
Follow HeimLabs for unapologetically practical Web3 dev content.
Twitter, LinkedIn.
Happy Building 🚀


Share Dialog
Share Dialog
HeimLabs
HeimLabs
1 comment
Stop backend key leaks. Build @solana bots that only sign approved txs with @coinbasedev Wallets v2 Policies. 3 rules, 1 SOL cap, full TypeScript demo: