Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.circle.com/llms.txt

Use this file to discover all available pages before exploring further.

USDC on Stellar is a native asset issued by Circle. Transferring USDC between Stellar accounts uses a standard payment operation, submitted through Horizon, Stellar’s HTTP API for submitting transactions. To transfer USDC crosschain between Stellar and other blockchains, see Transfer USDC to and from Stellar. The @stellar/stellar-sdk script you build in this guide will:
  • Create and fund a recipient Stellar Testnet wallet
  • Establish a USDC trustline on the recipient account
  • Transfer USDC from your existing sender wallet to the recipient
Before a Stellar account can receive USDC, it must establish a trustline for the asset. See Set up a USDC trustline on Stellar for more detail. This quickstart handles trustline setup automatically as part of the script.

Prerequisites

Before you begin, ensure that you have:
  • Installed Node.js v22+
  • Set up a terminal and code editor for running commands and editing files
  • Created a Stellar Testnet wallet with the secret key (S...) available

Contract addresses

You need the following Stellar Testnet USDC issuer address:

Step 1: Set up the project

1.1. Create the project and install dependencies

Create a new directory and install the required dependencies:
Shell
# Set up your directory and initialize a Node.js project
mkdir stellar-usdc-transfer
cd stellar-usdc-transfer
npm init -y

# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env main.ts"

# Install runtime dependencies
npm install @stellar/stellar-sdk

# Install dev dependencies
npm install --save-dev typescript @types/node

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
npx tsc --init
Then, update the tsconfig.json file:
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.3. Configure environment variables

Create a .env file in your project directory and add your sender’s secret key:
.env
STELLAR_SECRET_KEY=YOUR_SENDER_SECRET_KEY
Open .env in your editor rather than writing values with shell commands, and add .env to your .gitignore. This prevents credentials from leaking into your shell history or version control.
This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys.

Step 2: Create the transfer script

Add main.ts at the project root. The script:
  1. Loads your existing sender wallet from the environment variable
  2. Creates and funds a new recipient wallet using Friendbot
  3. Establishes a USDC trustline on the recipient account
  4. Submits a USDC payment from the sender to the recipient
main.ts
import {
  Asset,
  BASE_FEE,
  Horizon,
  Keypair,
  Networks,
  Operation,
  TransactionBuilder,
} from "@stellar/stellar-sdk";

const HORIZON_URL = "https://horizon-testnet.stellar.org";
const FRIENDBOT_URL = "https://friendbot.stellar.org";
const USDC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";
const USDC_CODE = "USDC";
const TRANSFER_AMOUNT = "10"; // 10 USDC
const TX_EXPLORER_BASE = "https://stellar.expert/explorer/testnet/tx";

const server = new Horizon.Server(HORIZON_URL);
const usdc = new Asset(USDC_CODE, USDC_ISSUER);

async function fundWithFriendbot(publicKey: string): Promise<void> {
  const response = await fetch(`${FRIENDBOT_URL}?addr=${publicKey}`);
  if (!response.ok) {
    throw new Error(
      `Friendbot funding failed: ${response.status} ${response.statusText}`,
    );
  }
}

async function establishTrustline(keypair: Keypair): Promise<void> {
  const account = await server.loadAccount(keypair.publicKey());
  const tx = new TransactionBuilder(account, {
    fee: BASE_FEE,
    networkPassphrase: Networks.TESTNET,
  })
    .addOperation(Operation.changeTrust({ asset: usdc }))
    .setTimeout(30)
    .build();

  tx.sign(keypair);
  const result = await server.submitTransaction(tx);
  if (!result.successful) {
    throw new Error(`Trustline transaction failed: ${JSON.stringify(result)}`);
  }
  console.log(`Trustline established: ${TX_EXPLORER_BASE}/${result.hash}`);
}

async function transferUsdc(
  sender: Keypair,
  recipientPublicKey: string,
): Promise<void> {
  const account = await server.loadAccount(sender.publicKey());
  const tx = new TransactionBuilder(account, {
    fee: BASE_FEE,
    networkPassphrase: Networks.TESTNET,
  })
    .addOperation(
      Operation.payment({
        destination: recipientPublicKey,
        asset: usdc,
        amount: TRANSFER_AMOUNT,
      }),
    )
    .setTimeout(30)
    .build();

  tx.sign(sender);
  const result = await server.submitTransaction(tx);
  if (!result.successful) {
    throw new Error(`Payment transaction failed: ${JSON.stringify(result)}`);
  }
  console.log(`Transfer successful: ${TX_EXPLORER_BASE}/${result.hash}`);
}

async function main() {
  // Load the sender wallet from the environment variable
  const sender = Keypair.fromSecret(process.env.STELLAR_SECRET_KEY as string);
  console.log("Sender address:", sender.publicKey());

  // Create and fund a new recipient wallet via Friendbot
  console.log("\nCreating recipient wallet...");
  const recipient = Keypair.random();
  console.log("Recipient address:", recipient.publicKey());

  console.log("Funding recipient with testnet XLM...");
  await fundWithFriendbot(recipient.publicKey());
  console.log("Recipient funded with XLM.");

  // Establish a USDC trustline on the recipient account
  console.log("\nEstablishing USDC trustline on recipient account...");
  await establishTrustline(recipient);

  // Transfer USDC from sender to recipient
  console.log(`\nTransferring ${TRANSFER_AMOUNT} USDC to recipient...`);
  await transferUsdc(sender, recipient.publicKey());

  // Confirm recipient balance
  const recipientAccount = await server.loadAccount(recipient.publicKey());
  const usdcBalance = recipientAccount.balances.find(
    (b) =>
      b.asset_type === "credit_alphanum4" &&
      b.asset_code === USDC_CODE &&
      b.asset_issuer === USDC_ISSUER,
  );
  console.log(`\nRecipient USDC balance: ${usdcBalance?.balance ?? "0"} USDC`);
}

main().catch(console.error);

Step 3: Run the script

From the project directory, run:
Shell
npm run start
Your output should look similar to the following (addresses and hashes will differ):
Shell
Sender address: GAXYZ...

Creating recipient wallet...
Recipient address: GDEFG...
Funding recipient with testnet XLM...
Recipient funded with XLM.

Establishing USDC trustline on recipient account...
Trustline established: https://stellar.expert/explorer/testnet/tx/abc123...

Transferring 10 USDC to recipient...
Transfer successful: https://stellar.expert/explorer/testnet/tx/def456...

Recipient USDC balance: 10.0000000 USDC
Common errors you might encounter:
  • Friendbot funding failed: 400 — The Friendbot rate limit was exceeded. Wait a few seconds and try again.
  • op_no_trust — The recipient account does not have a USDC trustline. The script establishes one automatically, but if you’re adapting this example for your own recipient address, ensure the trustline exists first. See Set up a USDC trustline on Stellar.
  • op_underfunded — The sender does not have enough USDC. Ensure your sender wallet is funded with testnet USDC from the Circle Faucet before running the script.
For more detail on Stellar-specific USDC behavior in CCTP, including address encoding and decimal precision, see CCTP on Stellar.