Skip to main content
This guide demonstrates how to transfer USDC between two wallets on Solana Devnet using a Node.js script. You use the Solana Kit library to interact with the Solana blockchain and perform SPL token transfers.
All transactions in this guide take place on the Solana Devnet testnet. No real funds are transferred. You can adapt this code for mainnet transactions by changing the RPC endpoint and using mainnet contract addresses.

Prerequisites

Before you start building the sample app to perform a USDC transfer, ensure you have:
  • Node.js and npm installed on your development machine. You can download and install Node.js directly, or use a version manager like nvm. The npm binary comes with Node.js.
  • Created two Solana wallets: a sender wallet (with the private key available) and a receiver wallet (you only need the public address).
  • Funded your sender wallet with the following testnet tokens:

Part 1: Project setup

Set up your project environment and install the required dependencies.

1.1. Create a new project

Create a new directory and initialize a new Node.js project with default settings:
Shell
mkdir solana-usdc-transfer
cd solana-usdc-transfer
npm init -y
npm pkg set type=module

1.2. Install dependencies

Install the required dependencies for Solana interactions:
Shell
npm install @solana/kit @solana-program/token ws dotenv

1.3. Configure environment variables

Create a .env file in your project directory and add your wallet private key and the receiver address:
Shell
echo "SOLANA_PRIVATE_KEY=[your-solana-private-key-array]" > .env
echo "RECEIVER_ADDRESS=your-receiver-wallet-address" >> .env
The SOLANA_PRIVATE_KEY should be a JSON array of bytes representing your private key. You can export this from most Solana wallets.
Some wallets export Solana private keys as Base58 encoded strings. If you have a Base58 encoded private key, you can convert it to a JSON array with the following Node.js script:
JavaScript
const fs = require("fs");
const bs58 = require("bs58");
const privateKeyBase58 = "YOUR_BASE58_PRIVATE_KEY";

try {
  // Decode the Base58 string to a Uint8Array
  const privateKeyBytes = bs58.decode(privateKeyBase58);

  // The result is a Uint8Array. Convert it to a JSON array string.
  const privateKeyJsonString = JSON.stringify(Array.from(privateKeyBytes));

  console.log("JSON Array:", privateKeyJsonString);
} catch (error) {
  console.error("Error converting key. Check if the Base58 key is valid.");
}
This is strictly for testing purposes. Never share your private keys or commit them to version control.

Part 2: Configure the script

Define the configuration constants for interacting with Solana Devnet.

2.1. Define configuration constants

The script predefines the RPC endpoints, USDC mint address, and transfer parameters:
JavaScript
import {
  address,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  getAddressEncoder,
  getProgramDerivedAddress,
  getSignatureFromTransaction,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import {
  TOKEN_PROGRAM_ADDRESS,
  getTransferInstruction,
  findAssociatedTokenPda,
} from "@solana-program/token";
import dotenv from "dotenv";

dotenv.config();

// Solana Devnet configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);

// Parse Solana keypair from environment variable
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY);
const senderKeypair = await createKeyPairSignerFromBytes(
  Uint8Array.from(solanaPrivateKey),
);

// USDC Mint Address (Devnet)
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");

// Transfer parameters
const RECEIVER_ADDRESS = address(process.env.RECEIVER_ADDRESS);
const AMOUNT = 1_000_000n; // 1 USDC (6 decimals)
USDC uses 6 decimal places. To transfer 1 USDC, you specify 1_000_000 (1 * 106) as the amount.

Part 3: Implement the transfer logic

The following sections outline the core transfer logic for sending USDC on Solana.

3.1. Get token accounts

Derive the Associated Token Account (ATA) addresses for both the sender and receiver. These accounts are program-derived addresses that hold SPL tokens for a specific wallet:
JavaScript
async function getTokenAccounts() {
  // Derive sender's USDC token account
  const [senderTokenAccount] = await findAssociatedTokenPda({
    mint: USDC_MINT,
    owner: senderKeypair.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  // Derive receiver's USDC token account
  const [receiverTokenAccount] = await findAssociatedTokenPda({
    mint: USDC_MINT,
    owner: RECEIVER_ADDRESS,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  return { senderTokenAccount, receiverTokenAccount };
}

3.2. Transfer USDC

Build and send the transfer transaction using the SPL Token program’s transfer instruction:
JavaScript
async function transferUSDC() {
  console.log("Transferring USDC on Solana Devnet...");
  console.log(`From: ${senderKeypair.address}`);
  console.log(`To: ${RECEIVER_ADDRESS}`);
  console.log(`Amount: ${Number(AMOUNT) / 1_000_000} USDC`);

  const { senderTokenAccount, receiverTokenAccount } = await getTokenAccounts();

  // Create the transfer instruction
  const transferInstruction = getTransferInstruction({
    source: senderTokenAccount,
    destination: receiverTokenAccount,
    authority: senderKeypair,
    amount: AMOUNT,
  });

  // Get the latest blockhash for transaction lifetime
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  // Build the transaction message
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(senderKeypair, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstruction(transferInstruction, tx),
  );

  // Sign and send the transaction
  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);

  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  await sendAndConfirmTransaction(signedTransaction, {
    commitment: "confirmed",
  });

  const signature = getSignatureFromTransaction(signedTransaction);
  console.log(`\nTransaction successful!`);
  console.log(`Signature: ${signature}`);
  console.log(
    `View on explorer: https://explorer.solana.com/tx/${signature}?cluster=devnet`,
  );

  return signature;
}

Part 4: Complete script

Create a transfer.js file in your project directory and populate it with the complete code below.
transfer.js
import {
  address,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  getSignatureFromTransaction,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import {
  TOKEN_PROGRAM_ADDRESS,
  getTransferInstruction,
  findAssociatedTokenPda,
} from "@solana-program/token";
import dotenv from "dotenv";

dotenv.config();

// Solana Devnet configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);

// Parse Solana keypair from environment variable
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY);
const senderKeypair = await createKeyPairSignerFromBytes(
  Uint8Array.from(solanaPrivateKey),
);

// USDC Mint Address (Devnet)
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");

// Transfer parameters
const RECEIVER_ADDRESS = address(process.env.RECEIVER_ADDRESS);
const AMOUNT = 1_000_000n; // 1 USDC (6 decimals)

async function getTokenAccounts() {
  // Derive sender's USDC token account
  const [senderTokenAccount] = await findAssociatedTokenPda({
    mint: USDC_MINT,
    owner: senderKeypair.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  // Derive receiver's USDC token account
  const [receiverTokenAccount] = await findAssociatedTokenPda({
    mint: USDC_MINT,
    owner: RECEIVER_ADDRESS,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  return { senderTokenAccount, receiverTokenAccount };
}

async function transferUSDC() {
  console.log("Transferring USDC on Solana Devnet...");
  console.log(`From: ${senderKeypair.address}`);
  console.log(`To: ${RECEIVER_ADDRESS}`);
  console.log(`Amount: ${Number(AMOUNT) / 1_000_000} USDC`);

  const { senderTokenAccount, receiverTokenAccount } = await getTokenAccounts();

  // Create the transfer instruction
  const transferInstruction = getTransferInstruction({
    source: senderTokenAccount,
    destination: receiverTokenAccount,
    authority: senderKeypair,
    amount: AMOUNT,
  });

  // Get the latest blockhash for transaction lifetime
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  // Build the transaction message
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(senderKeypair, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstruction(transferInstruction, tx),
  );

  // Sign and send the transaction
  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);

  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  await sendAndConfirmTransaction(signedTransaction, {
    commitment: "confirmed",
  });

  const signature = getSignatureFromTransaction(signedTransaction);
  console.log(`\nTransaction successful!`);
  console.log(`Signature: ${signature}`);
  console.log(
    `View on explorer: https://explorer.solana.com/tx/${signature}?cluster=devnet`,
  );

  return signature;
}

transferUSDC().catch(console.error);

Part 5: Test the script

Run the following command to execute the script:
Shell
node transfer.js
Once the script runs and the transfer is finalized, a confirmation message is logged in the console:
Transferring USDC on Solana Devnet...
From: YourSenderAddress
To: YourReceiverAddress
Amount: 1 USDC

Transaction successful!
Signature: 5abc123...
View on explorer: https://explorer.solana.com/tx/5abc123...?cluster=devnet
You can verify the transaction by visiting the Solana Explorer link in the output, or by checking the USDC balance in the receiver’s wallet.
If the receiver’s wallet has never held USDC before, you may need to create their Associated Token Account first. The transfer will fail if the destination token account does not exist. You can use the getOrCreateAssociatedTokenAccount pattern to handle this automatically.