Skip to main content
In this guide, you build a script that:
  • Sends USDC from an dev-controlled wallet to a recipient address.
  • Requires the source wallet to pay gas fees for the transfer transaction.
  • Verifies the updated token balance and recent outbound transactions.
Circle offers Gas Station for you to enable gas sponsorship. View Send a Gasless Transaction to learn how to send a gasless transaction.

Prerequisites

Before you begin, ensure you have:

Step 1. Set up your project

Skip this step if you completed the Create a Dev-Controlled Wallet quickstart. You can reuse the dev-controlled-projects project folder and its .env file.
Create a .env file in your project directory and add the following variables:
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
CIRCLE_WALLET_ADDRESS=YOUR_WALLET_ADDRESS
CIRCLE_WALLET_BLOCKCHAIN=YOUR_WALLET_BLOCKCHAIN
Where:
  • YOUR_API_KEY: Your Circle Developer API key
  • YOUR_ENTITY_SECRET: Your Circle Developer
  • YOUR_WALLET_ADDRESS: Wallet source address (used for the transfer)
  • YOUR_WALLET_BLOCKCHAIN: Wallet blockchain (for example, ARC-TESTNET). The script derives the wallet ID from this and the wallet address using the API.

Step 2. Create the script

Create a file named send-assets.ts in your project folder. Each subsection below adds a new functionality. If you prefer, you can skip ahead and copy the full script.

2.1. Send the transfer

Add the imports and the code that transfers USDC to a recipient address, then polls until the transaction completes:
send-assets.ts
// send-assets.ts

import {
  initiateDeveloperControlledWalletsClient,
  type TokenBlockchain,
  type EvmBlockchain,
} from "@circle-fin/developer-controlled-wallets";

// Hardcoded recipient address; replace with any valid Arc Testnet address.
const DESTINATION_ADDRESS = "0xb505c4ad888c05bc8c6f2bf237f57f2b1a11a0d2";
const TRANSFER_AMOUNT_USDC = "0.5";
const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000";

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function main() {
  const apiKey = process.env.CIRCLE_API_KEY;
  const entitySecret = process.env.CIRCLE_ENTITY_SECRET;
  const walletAddress = process.env.CIRCLE_WALLET_ADDRESS;
  const blockchain = process.env.CIRCLE_WALLET_BLOCKCHAIN;
  if (!apiKey || !entitySecret || !walletAddress || !blockchain) {
    throw new Error(
      "CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ADDRESS, and CIRCLE_WALLET_BLOCKCHAIN are required in .env"
    );
  }

  const client = initiateDeveloperControlledWalletsClient({
    apiKey,
    entitySecret,
  });

  const deriveResponse = await client.deriveWalletByAddress({
    sourceBlockchain: blockchain as EvmBlockchain,
    walletAddress,
    targetBlockchain: blockchain as EvmBlockchain,
  });
  const walletId = deriveResponse.data?.wallet?.id;
  if (!walletId) {
    throw new Error("deriveWalletByAddress: no wallet id in response");
  }

  console.log("\nSender address:", walletAddress);
  console.log("Recipient address:", DESTINATION_ADDRESS);
  console.log(`\nSending ${TRANSFER_AMOUNT_USDC} USDC to recipient...`);
  const txResponse = await client.createTransaction({
    blockchain: blockchain as TokenBlockchain,
    walletAddress,
    destinationAddress: DESTINATION_ADDRESS,
    amount: [TRANSFER_AMOUNT_USDC],
    tokenAddress: ARC_TESTNET_USDC,
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });

  const txId = txResponse.data?.id;
  const txState = txResponse.data?.state;
  if (!txId) {
    throw new Error("Transaction creation failed: no ID returned");
  }
  console.log("Transaction ID:", txId);
  console.log("Initial State:", txState);

  // Poll until transaction is COMPLETE (or terminal)
  const terminalStates = new Set(["COMPLETE", "FAILED", "CANCELLED", "DENIED"]);
  let currentState = txState;

  while (!terminalStates.has(currentState)) {
    await sleep(3000);
    const pollResponse = await client.getTransaction({ id: txId });
    const tx = pollResponse.data?.transaction;
    currentState = tx?.state ?? "";
    console.log("State:", currentState);

    if (currentState === "COMPLETE" && tx?.txHash) {
      console.log(`Explorer: https://testnet.arcscan.app/tx/${tx.txHash}`);
    }
  }

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

2.2. Verify the balance

Add the code that fetches the sender’s token balance and lists recent outbound transactions:
send-assets.ts
  // Verify sender's token balance
  console.log("\nToken Balances:");
  const balanceResponse = await client.getWalletTokenBalance({ id: walletId });
  const tokenBalances = balanceResponse.data?.tokenBalances;

  if (!tokenBalances || tokenBalances.length === 0) {
    console.log("  No token balances found.");
  } else {
    for (const balance of tokenBalances) {
      console.log(`  ${balance.token?.symbol ?? "Unknown"}: ${balance.amount}`);
    }
  }

  // List recent outbound transactions
  console.log("\nOutbound Transactions:");
  const txListResponse = await client.listTransactions({
    walletIds: [walletId],
    txType: "OUTBOUND",
  });
  const transactions = txListResponse.data?.transactions;

  if (!transactions || transactions.length === 0) {
    console.log("  No outbound transactions found.");
  } else {
    for (const tx of transactions) {
      console.log(`  TX: https://testnet.arcscan.app/tx/${tx.txHash}`);
      console.log(`    To: ${tx.destinationAddress}`);
      console.log(`    Amount: ${tx.amounts?.join(", ") ?? "—"}`);
      console.log(`    State: ${tx.state}`);
      console.log(`    Date: ${tx.createDate}`);
      console.log();
    }
  }
}

main().catch((err) => {
  console.error("Error:", err.message || err);
  process.exit(1);
});

2.3. Copy the full script

Here is the complete send-assets.ts:
send-assets.ts
/**
 * send-assets.ts: Send a Transaction to Transfer Assets
 *
 * 1. Transfer USDC to a recipient address
 * 2. Verify the sender's token balance
 * 3. List recent outbound transactions
 *
 * Required .env (or environment):
 *   CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET,
 *   CIRCLE_WALLET_ADDRESS, CIRCLE_WALLET_BLOCKCHAIN
 *
 * Usage:
 *   node --env-file=.env --import=tsx send-assets.ts
 */

import {
  initiateDeveloperControlledWalletsClient,
  type TokenBlockchain,
  type EvmBlockchain,
} from "@circle-fin/developer-controlled-wallets";

// Replace with any valid Arc Testnet address.
const DESTINATION_ADDRESS = "0xb505c4ad888c05bc8c6f2bf237f57f2b1a11a0d2";
const TRANSFER_AMOUNT_USDC = "0.5";
const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000";

async function main() {
  const apiKey = process.env.CIRCLE_API_KEY;
  const entitySecret = process.env.CIRCLE_ENTITY_SECRET;
  const walletAddress = process.env.CIRCLE_WALLET_ADDRESS;
  const blockchain = process.env.CIRCLE_WALLET_BLOCKCHAIN;
  if (!apiKey || !entitySecret || !walletAddress || !blockchain) {
    throw new Error(
      "CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ADDRESS, and CIRCLE_WALLET_BLOCKCHAIN are required in .env",
    );
  }

  const client = initiateDeveloperControlledWalletsClient({
    apiKey,
    entitySecret,
  });

  const deriveResponse = await client.deriveWalletByAddress({
    sourceBlockchain: blockchain as EvmBlockchain,
    walletAddress,
    targetBlockchain: blockchain as EvmBlockchain,
  });
  const walletId = deriveResponse.data?.wallet?.id;
  if (!walletId) {
    throw new Error("deriveWalletByAddress: no wallet id in response");
  }

  // Transfer USDC
  console.log("Sender address:", walletAddress);
  console.log("Recipient address:", DESTINATION_ADDRESS);
  console.log(`\nSending ${TRANSFER_AMOUNT_USDC} USDC to recipient...`);
  const txResponse = await client.createTransaction({
    blockchain: blockchain as TokenBlockchain,
    walletAddress,
    destinationAddress: DESTINATION_ADDRESS,
    amount: [TRANSFER_AMOUNT_USDC],
    tokenAddress: ARC_TESTNET_USDC,
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });

  const txId = txResponse.data?.id;
  if (!txId) throw new Error("Transaction creation failed: no ID returned");
  console.log("Transaction ID:", txId);

  // Poll until transaction reaches a terminal state
  let currentState: string | undefined = txResponse.data?.state;
  while (
    !currentState ||
    !["COMPLETE", "FAILED", "CANCELLED", "DENIED"].includes(currentState)
  ) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const poll = await client.getTransaction({ id: txId });
    const tx = poll.data?.transaction;
    currentState = tx?.state;
    console.log("Transaction state:", currentState);
    if (currentState === "COMPLETE" && tx?.txHash) {
      console.log(`Explorer: https://testnet.arcscan.app/tx/${tx.txHash}`);
    }
  }
  if (currentState !== "COMPLETE") {
    throw new Error(`Transaction ended in state: ${currentState}`);
  }

  // Verify sender's token balance
  console.log("\nToken Balances:");
  const balanceResponse = await client.getWalletTokenBalance({ id: walletId });
  const tokenBalances = balanceResponse.data?.tokenBalances;
  if (!tokenBalances || tokenBalances.length === 0) {
    console.log("  No token balances found.");
  } else {
    for (const balance of tokenBalances) {
      console.log(`  ${balance.token?.symbol ?? "Unknown"}: ${balance.amount}`);
    }
  }

  // List recent outbound transactions
  console.log("\nOutbound Transactions:");
  const txListResponse = await client.listTransactions({
    walletIds: [walletId],
    txType: "OUTBOUND",
  });
  const transactions = txListResponse.data?.transactions;
  if (!transactions || transactions.length === 0) {
    console.log("  No outbound transactions found.");
  } else {
    for (const tx of transactions) {
      console.log(`  TX: https://testnet.arcscan.app/tx/${tx.txHash}`);
      console.log(`    To: ${tx.destinationAddress}`);
      console.log(`    Amount: ${tx.amounts?.join(", ") ?? "—"}`);
      console.log(`    State: ${tx.state}`);
      console.log(`    Date: ${tx.createDate}`);
      console.log();
    }
  }
}

main().catch((err) => {
  console.error("Error:", err.message || err);
  process.exit(1);
});

Step 3. Run the script

From your project folder (the same directory as send-assets.ts and .env), run:
node --env-file=.env --import=tsx send-assets.ts
You should see output similar to:
Sending 0.5 USDC to recipient...
Transaction ID: ...
Transaction state: COMPLETE
Explorer: https://testnet.arcscan.app/tx/0x...

Token Balances:
  USDC: ...

Outbound Transactions:
  TX: https://testnet.arcscan.app/tx/0x...
    To: ...
    Amount: ...
    State: COMPLETE
    Date: ...
The balance may be slightly lower than before the transfer because of gas fees.