Skip to main content

Overview

This guide covers the fundamentals of sending transactions on Solana using the CDP v2 Server Wallet. You will learn how to construct transactions and submit them to the Solana network. Solana transactions benefit from a single API that supports signing and broadcasting. With CDP, signing keys are securely managed in the platform’s infrastructure.
  1. Create transaction: Build a transaction with one or more instructions
  2. Send transaction: Use CDP to send the serialized transaction
  3. Confirm transaction: Wait for network confirmation

Prerequisites

It is assumed you have:
  • Completed the Quickstart guide
  • Basic understanding of Solana accounts
  • Installed dependencies:
    • For TypeScript: @solana/web3.js, @coinbase/cdp-sdk, and dotenv
    • For Python: solana, solders, cdp-sdk, and python-dotenv
The following steps break down the transaction flow into digestible pieces. If you prefer to see the full working code immediately, skip to the Complete example section below.

1. Create a Solana account

First, create or retrieve a Solana account using CDP. The below example uses solana-devnet and will source SOL from CDP faucet to transfer.
import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";

const cdp = new CdpClient();

const account = await cdp.solana.createAccount({
  name: "test-sol-account",
});

let fromAddress: string;
fromAddress = account.address;
console.log("Successfully created new SOL account:", fromAddress);

// Request SOL from faucet
const faucetResp = await cdp.solana.requestFaucet({
  address: fromAddress,
  token: "sol",
});
console.log(
  "Successfully requested SOL from faucet:",
  faucetResp.signature
);

2. Build the transaction

Prepare a transaction with one or more instructions. The transaction may contain several instructions, each of which may require signatures from different account keys. Here is a simple SOL transfer.
We set the blockhash to a static value temporarily since the CDP v2 Server Wallet will update it before it is sent to the network.
import {
  PublicKey,
  SystemProgram,
  SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
  Transaction,
} from "@solana/web3.js";

// Required: Destination address to send SOL to (replace with your recipient)
const destinationAddress = "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE";

// Amount of lamports to send (default: 1000 = 0.000001 SOL)
const lamportsToSend = 1000;

// Assumes fromAddress is defined from step 1
let fromAddress: string; // Your Solana account address

const transaction = new Transaction();
transaction.add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(fromAddress),
    toPubkey: new PublicKey(destinationAddress),
    lamports: lamportsToSend,
  })
);
transaction.recentBlockhash = SYSVAR_RECENT_BLOCKHASHES_PUBKEY.toBase58();
transaction.feePayer = new PublicKey(fromAddress);

3. Serialize transaction

Serialize the transaction for signing:
const serializedTx = Buffer.from(
  transaction.serialize({ requireAllSignatures: false })
).toString("base64");

console.log("Transaction serialized successfully");

4. Send the transaction

Use CDP to send the serialized transaction:
import { CdpClient } from "@coinbase/cdp-sdk";

// Assumes cdp client is initialized from step 1
const cdp = new CdpClient();

const txResult = await cdp.solana.sendTransaction({
    network: "solana-devnet",
    transaction: serializedTx,
});

const signature = txResult.signature;
console.log("Solana transaction hash:", signature);

5. Confirm

Wait for confirmation after you submitted the transaction to the network.
console.log("Waiting for transaction to be confirmed");
const latestBlockhash = await connection.getLatestBlockhash();
const confirmation = await connection.confirmTransaction({
  signature,
  blockhash: latestBlockhash.blockhash,
  lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
});

if (confirmation.value.err) {
  throw new Error(
    `Transaction failed: ${confirmation.value.err.toString()}`
  );
}

console.log(
  "Transaction confirmed:",
  confirmation.value.err ? "failed" : "success"
);
console.log(
  `Transaction explorer link: https://explorer.solana.com/tx/${signature}?cluster=devnet`
);

Complete example

Here’s the complete, runnable example for easy copy and paste:
// Usage: pnpm tsx solana/sendTransaction.ts [sourceAddress]

import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";

import {
    Connection,
    PublicKey,
    SystemProgram,
    SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
    Transaction,
} from "@solana/web3.js";

/**
 * This script will:
 * 1. Either use a provided Solana address or create a new one
 * 2. If a new account is created, requests SOL from CDP faucet
 * 3. Creates and sends a transaction with CDP to send SOL to a destination address
 * 4. Waits for transaction confirmation
 *
 * @param {string} [sourceAddress] - The source address to use
 * @returns A promise that resolves when the transaction is confirmed
 */
async function main(sourceAddress?: string) {
    const cdp = new CdpClient();

    // Required: Destination address to send SOL to
    const destinationAddress = "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE";

    // Amount of lamports to send (default: 1000 = 0.000001 SOL)
    const lamportsToSend = 1000;

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

        let fromAddress: string;
        if (sourceAddress) {
            fromAddress = sourceAddress;
            console.log("Using existing SOL account:", fromAddress);
        } else {
            const account = await cdp.solana.createAccount({
                name: "test-sol-account",
            });

            fromAddress = account.address;
            console.log("Successfully created new SOL account:", fromAddress);

            // Request SOL from faucet
            const faucetResp = await cdp.solana.requestFaucet({
                address: fromAddress,
                token: "sol",
            });
            console.log(
                "Successfully requested SOL from faucet:",
                faucetResp.signature
            );
        }

        // Wait until the address has balance
        let balance = 0;
        let attempts = 0;
        const maxAttempts = 30;

        while (balance === 0 && attempts < maxAttempts) {
            balance = await connection.getBalance(new PublicKey(fromAddress));
            if (balance === 0) {
                console.log("Waiting for funds...");
                await sleep(1000);
                attempts++;
            }
        }

        if (balance === 0) {
            throw new Error("Account not funded after multiple attempts");
        }

        console.log("Account funded with", balance / 1e9, "SOL");

        if (balance < lamportsToSend) {
            throw new Error(
                `Insufficient balance: ${balance} lamports, need at least ${lamportsToSend} lamports`
            );
        }

        const transaction = new Transaction();
        transaction.add(
            SystemProgram.transfer({
                fromPubkey: new PublicKey(fromAddress),
                toPubkey: new PublicKey(destinationAddress),
                lamports: lamportsToSend,
            })
        );

        // A more recent blockhash is set in the backend by CDP
        transaction.recentBlockhash = SYSVAR_RECENT_BLOCKHASHES_PUBKEY.toBase58()
        transaction.feePayer = new PublicKey(fromAddress);

        const serializedTx = Buffer.from(
            transaction.serialize({ requireAllSignatures: false })
        ).toString("base64");

        console.log("Transaction serialized successfully");

        const txResult = await cdp.solana.sendTransaction({
            network: "solana-devnet",
            transaction: serializedTx,
        });

        const signature = txResult.signature;
        console.log("Solana transaction hash:", signature);

        console.log("Waiting for transaction to be confirmed");
        const latestBlockhash = await connection.getLatestBlockhash();
        const confirmation = await connection.confirmTransaction({
            signature,
            blockhash: latestBlockhash.blockhash,
            lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
        });

        if (confirmation.value.err) {
            throw new Error(
                `Transaction failed: ${confirmation.value.err.toString()}`
            );
        }

        console.log(
            "Transaction confirmed:",
            confirmation.value.err ? "failed" : "success"
        );
        console.log(
            `Transaction explorer link: https://explorer.solana.com/tx/${signature}?cluster=devnet`
        );

        return {
            fromAddress,
            destinationAddress,
            amount: lamportsToSend / 1e9,
            signature,
            success: !confirmation.value.err,
        };
    } catch (error) {
        console.error("Error processing SOL transaction:", error);
        throw error;
    }
}

/**
 * Sleeps for a given number of milliseconds
 *
 * @param {number} ms - The number of milliseconds to sleep
 * @returns {Promise<void>} A promise that resolves when the sleep is complete
 */
function sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

const sourceAddress = process.argv.length > 2 ? process.argv[2] : undefined;

main(sourceAddress).catch(console.error);
More code samples are available in our TypeScript and Python SDK repositories.

Optimizing transaction sends

Priority Fee

When sending a transaction on Solana, you can set a priority fee, which is an optional fee that increases the likelihood of your transaction being included in the next block by validators. The priority fee is calculated based on the compute unit price and compute unit limit of the transaction. Priority Fee = Compute Unit Limit × Compute Unit Price Read more about Solana’s fee model.
CDP v2 Server Wallet will automatically add the appropriate instructions for the compute unit limit and compute unit price on a transaction if not already present.
To set your own compute unit limit and compute unit price for a transaction, take a look at the examples below.

Compute Unit Price

import { ComputeBudgetProgram, Transaction } from "@solana/web3.js";

// Assume you already have `otherInstruction`.
// Build a tx with compute unit price first, then your instruction(s).
const computeUnitPriceInstruction = ComputeBudgetProgram.setComputeUnitPrice({
  microLamports: 5000,
});

const tx = new Transaction().add(priorityFeeInstruction, otherInstruction);

// Send tx with your usual send flow.

Compute Unit Limit

import { ComputeBudgetProgram, Transaction } from "@solana/web3.js";

// Assume you already have `otherInstruction`.
// Build a tx with compute unit limit first, then your instruction(s).
const computeLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({
  units: 300000,
});

const tx = new Transaction().add(computeLimitInstruction, otherInstruction);

// Send tx with your usual send flow.

Bring your own node

Use a custom node provider to build and broadcast the transaction instead of relying on the CDP SDK. You will still call CDP to sign the transaction, but you can query the recent blockhash and broadcast the transaction using your own node.
When you call cdp.solana.signTransaction(), you send an unsigned transaction to CDP’s secure infrastructure. CDP signs it using your managed private key (which never leaves the Trusted Execution Environment) and returns the signed transaction.
// Usage: pnpm tsx solana/signAndSendTransaction.ts [sourceAddress]

import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";

import {
  Connection,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";

/**
 * This script will:
 * 1. Either use a provided Solana address or create a new one
 * 2. If a new account is created, requests SOL from CDP faucet
 * 3. Signs a transaction with CDP to send SOL to a destination address
 * 4. Broadcasts the signed transaction
 *
 * @param {string} [sourceAddress] - The source address to use
 * @returns A promise that resolves when the transaction is confirmed
 */
async function main(sourceAddress?: string) {
  const cdp = new CdpClient();

  // Required: Destination address to send SOL to (replace with your recipient)
  const destinationAddress = "3KzDtddx4i53FBkvCzuDmRbaMozTZoJBb1TToWhz3JfE";

  // Amount of lamports to send (default: 1000 = 0.000001 SOL)
  const lamportsToSend = 1000;

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

    let fromAddress: string;
    if (sourceAddress) {
      fromAddress = sourceAddress;
      console.log("Using existing SOL account:", fromAddress);
    } else {
      const account = await cdp.solana.createAccount({
        name: "test-sol-account",
      });

      fromAddress = account.address;
      console.log("Successfully created new SOL account:", fromAddress);

      // Request SOL from faucet
      const faucetResp = await cdp.solana.requestFaucet({
        address: fromAddress,
        token: "sol",
      });
      console.log(
        "Successfully requested SOL from faucet:",
        faucetResp.signature
      );
    }

    // Wait until the address has balance
    let balance = 0;
    let attempts = 0;
    const maxAttempts = 30;

    while (balance === 0 && attempts < maxAttempts) {
      balance = await connection.getBalance(new PublicKey(fromAddress));
      if (balance === 0) {
        console.log("Waiting for funds...");
        await sleep(1000);
        attempts++;
      }
    }

    if (balance === 0) {
      throw new Error("Account not funded after multiple attempts");
    }

    console.log("Account funded with", balance / 1e9, "SOL");

    if (balance < lamportsToSend) {
      throw new Error(
        `Insufficient balance: ${balance} lamports, need at least ${lamportsToSend} lamports`
      );
    }

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

    const transaction = new Transaction();
    transaction.add(
      SystemProgram.transfer({
        fromPubkey: new PublicKey(fromAddress),
        toPubkey: new PublicKey(destinationAddress),
        lamports: lamportsToSend,
      })
    );

    transaction.recentBlockhash = blockhash;
    transaction.feePayer = new PublicKey(fromAddress);

    const serializedTx = Buffer.from(
      transaction.serialize({ requireAllSignatures: false })
    ).toString("base64");

    console.log("Transaction serialized successfully");

    const signedTxResponse = await cdp.solana.signTransaction({
      address: fromAddress,
      transaction: serializedTx,
    });

    const decodedSignedTx = Buffer.from(signedTxResponse.signature, "base64");

    const signature = await connection.sendRawTransaction(decodedSignedTx);
    console.log("Solana transaction hash:", signature);

    console.log("Waiting for transaction to be confirmed");
    const latestBlockhash = await connection.getLatestBlockhash();
    const confirmation = await connection.confirmTransaction({
      signature,
      blockhash: latestBlockhash.blockhash,
      lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
    });

    if (confirmation.value.err) {
      throw new Error(
        `Transaction failed: ${confirmation.value.err.toString()}`
      );
    }

    console.log(
      "Transaction confirmed:",
      confirmation.value.err ? "failed" : "success"
    );
    console.log(
      `Transaction explorer link: https://explorer.solana.com/tx/${signature}?cluster=devnet`
    );

    return {
      fromAddress,
      destinationAddress,
      amount: lamportsToSend / 1e9,
      signature,
      success: !confirmation.value.err,
    };
  } catch (error) {
    console.error("Error processing SOL transaction:", error);
    throw error;
  }
}

/**
 * Sleeps for a given number of milliseconds
 *
 * @param {number} ms - The number of milliseconds to sleep
 * @returns {Promise<void>} A promise that resolves when the sleep is complete
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const sourceAddress = process.argv.length > 2 ? process.argv[2] : undefined;

main(sourceAddress).catch(console.error);
I