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.

Developer-controlled wallets can send tokens to any address on the same blockchain. After completing this tutorial, you’ll have sent USDC from one developer-controlled wallet to
another. The examples use Arc Testnet, but you can send tokens on any supported blockchain.
Circle offers Gas Station if you want to sponsor gas instead of funding the sender wallet directly. See Send a Gasless Transaction.

Prerequisites

Before you begin, ensure you have:

Step 1. Set up your project

Reuse the dev-controlled-projects folder you created in the Create a Dev-Controlled Wallet quickstart.

1.1. Prepare your project

Add the transfer run command:
# Add a script for the transfer quickstart
npm pkg set scripts.send-tokens="tsx --env-file=.env send-tokens.ts"

1.2. Set environment variables

Add your API key and entity secret to .env:
.env
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
  • CIRCLE_API_KEY is your Circle Developer API key.
  • CIRCLE_ENTITY_SECRET is your registered entity secret.
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.

Step 2. Send USDC between wallets

Write a script that sends USDC from the source wallet, polls for completion, then checks the recipient balance.

2.1. Create the script

Create a send-tokens.ts (or send_tokens.py) file and add the following code. The script calls createTransaction() to initiate the transfer, then polls getTransaction() until the transaction reaches a terminal state: COMPLETE, FAILED, CANCELLED, or DENIED.
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

// Replace the source and destination constants with your own wallet values
const SOURCE_WALLET_ADDRESS: string = "0x..."; // Used with blockchain to identify the source wallet
const SOURCE_WALLET_BLOCKCHAIN = "0x..."; // Used with blockchain to identify the source wallet
const DESTINATION_WALLET_ADDRESS: string = "0x..."; // Recipient wallet address
const DESTINATION_WALLET_ID: string = "..."; // Used for post-transfer balance check only
const ARC_TESTNET_USDC: string = "0x3600000000000000000000000000000000000000";
const TRANSFER_AMOUNT_USDC: string = "5"; // Token quantity as a string

// Initialize the wallets client
const client = initiateDeveloperControlledWalletsClient({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

async function main() {
  // Validate the wallet inputs
  if (
    SOURCE_WALLET_ADDRESS === "YOUR_SOURCE_WALLET_ADDRESS" ||
    DESTINATION_WALLET_ID === "YOUR_DESTINATION_WALLET_ID" ||
    DESTINATION_WALLET_ADDRESS === "YOUR_DESTINATION_WALLET_ADDRESS"
  ) {
    throw new Error(
      "Replace the wallet constants at the top of send-tokens.ts before running the script.",
    );
  }

  // Create the transfer transaction
  const transferResponse = await client.createTransaction({
    blockchain: SOURCE_WALLET_BLOCKCHAIN,
    walletAddress: SOURCE_WALLET_ADDRESS,
    tokenAddress: ARC_TESTNET_USDC, // USDC contract address on Arc Testnet; replace for other chains
    destinationAddress: DESTINATION_WALLET_ADDRESS,
    amount: [TRANSFER_AMOUNT_USDC],
    fee: {
      type: "level",
      config: { feeLevel: "MEDIUM" }, // Gas fee strategy: LOW, MEDIUM, or HIGH
    },
  });

  const transactionId = transferResponse.data?.id;
  let currentState = transferResponse.data?.state ?? "";

  if (!transactionId) {
    throw new Error("Transaction creation failed: no ID returned");
  }

  console.log("Transfer response:", transferResponse.data);

  // Wait for the transfer to finish
  const terminalStates = new Set(["COMPLETE", "FAILED", "CANCELLED", "DENIED"]);

  while (!terminalStates.has(currentState)) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const pollResponse = await client.getTransaction({ id: transactionId });
    const tx = pollResponse.data?.transaction;
    currentState = tx?.state ?? "";
    console.log("Transaction response:", pollResponse.data);

    if (currentState === "COMPLETE") break;
  }

  if (currentState !== "COMPLETE") {
    throw new Error(`Transaction ended in state: ${currentState}`);
  }

  // Check the recipient balance
  const destinationBalanceResponse = await client.getWalletTokenBalance({
    id: DESTINATION_WALLET_ID,
  });

  console.log("Wallet balance response:", destinationBalanceResponse.data);
}

main().catch((err) => {
  console.error("Error:", err.message || err);
  process.exit(1);
});
If you’re calling the API directly instead of using the SDK, use Create Transfer Transaction, Get Transaction, and List Wallet Balance. Be sure to replace the entity secret ciphertext and idempotency key in your request. If you’re using the SDKs, this is handled automatically for you.

2.2. Run the script

Run the script from your project directory:
npm run send-tokens
The output looks similar to:
Transfer response: {
  id: "6f10...",
  state: "INITIATED"
}
Transaction response: {
  transaction: {
    id: "6f10...",
    state: "COMPLETE",
    blockchain: "ARC-TESTNET",
    txHash: "0x..."
  }
}
Wallet balance response: {
  tokenBalances: [
    {
      token: [Object],
      amount: "5"
    }
  ]
}
You can also monitor the transfer through webhook notifications or by polling Get Transaction.

Next steps

  • Build payment workflows with Arc App Kit: Use the Circle Wallets adapter to add token transfers, swaps, bridging, and chain-agnostic unified balances to your app without building each integration yourself.