Skip to main content
This guide shows how to transfer USDC crosschain using the Circle Forwarding Service. This example shows a transfer from Base Sepolia to Avalanche Fuji, but you can use the same steps to transfer to any supported destination chain. When you use the Forwarding Service, Circle handles the mint transaction on the destination chain, eliminating the need for you to hold native tokens for gas on the destination chain or run multichain infrastructure.

Prerequisites

Before you start, ensure you have:
  • Installed Node.js and npm on your development machine.
  • Created a new Node.js project and installed the viem and dotenv packages.
  • Created a wallet with the private key available on the source chain.
  • Funded the wallet with testnet USDC and native tokens for gas fees on the source chain.
  • Created a .env file with your private key and recipient address.

Steps

Use the following steps to transfer USDC with the Forwarding Service.

Step 1. Get CCTP fees from the API

Query the CCTP API for the fees for transferring USDC from Base to Avalanche. This value is passed to the maxFee parameter in the depositForBurnWithHook transaction. The following is an example request using source domain 6 (Base) and destination domain 1 (Avalanche):
const response = await fetch(
  "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/1?forward=true",
  {
    method: "GET",
    headers: { "Content-Type": "application/json" },
  },
);

const fees = await response.json();
console.log(fees);
Response:
[
  {
    "finalityThreshold": 1000, // fast transfer
    "minimumFee": 1, // bps
    "forwardFee": {
      "low": 200000, // 0.20 USDC in USDC minor units
      "med": 200000,
      "high": 200000
    }
  },
  {
    "finalityThreshold": 2000, // standard transfer
    "minimumFee": 0,
    "forwardFee": {
      "low": 200000,
      "med": 200000,
      "high": 200000
    }
  }
]
The forwardFee is the fee charged by the Forwarding Service. The minimumFee is the CCTP protocol fee (in basis points).
Circle recommends selecting the med fee level or higher from the forwardFee object in the API response.

Step 2. Calculate the USDC amounts and fees

Calculate the total fee by combining the protocol fee and the Forwarding Service fee. The maxFee parameter must cover both fees for the transfer to succeed.
// Amount to transfer (10 USDC in minor units)
const transferAmount = 10_000_000n;

// Parse fees from API response
const feeData = fees[0]; // Use fast transfer fees (finalityThreshold: 1000)
const forwardFee = BigInt(feeData.forwardFee.med); // 200000 (0.20 USDC)

// Calculate protocol fee (minimumFee is in basis points)
const protocolFeeBps = BigInt(feeData.minimumFee);
const protocolFee = (transferAmount * protocolFeeBps) / 10000n;

// Total max fee should cover both fees
const maxFee = forwardFee + protocolFee;

console.log("Transfer amount:", transferAmount, "minor units");
console.log("Forward fee:", forwardFee, "minor units");
console.log("Protocol fee:", protocolFee, "minor units");
console.log("Max fee:", maxFee, "minor units");
For a 10 USDC transfer from Base to Avalanche, the total fee is 0.201 USDC (the Forwarding Service fee plus the CCTP protocol fee).
If the maxFee parameter is insufficient to cover the both Fast Transfer protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding execution over Fast Transfer. This means that the transfer will execute as a Standard Transfer with the Forwarding Service.

Step 3. Approve the USDC transfer

Grant approval for the TokenMessengerV2 contract deployed on Base to transfer USDC from your wallet. This allows the contract to burn USDC when you initiate the transfer.
import { createWalletClient, http, encodeFunctionData } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
  "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";

// Set up wallet client
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const client = createWalletClient({
  chain: baseSepolia,
  transport: http(),
  account,
});

async function approveUSDC(amount) {
  console.log("Approving USDC transfer...");
  const approveTx = await client.sendTransaction({
    to: BASE_SEPOLIA_USDC,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "approve",
          stateMutability: "nonpayable",
          inputs: [
            { name: "spender", type: "address" },
            { name: "amount", type: "uint256" },
          ],
          outputs: [{ name: "", type: "bool" }],
        },
      ],
      functionName: "approve",
      args: [BASE_SEPOLIA_TOKEN_MESSENGER, amount],
    }),
  });
  console.log("USDC Approval Tx:", approveTx);
  return approveTx;
}

Step 4. Sign and broadcast a depositForBurnWithHook transaction on the TokenMessengerV2 contract

Create and send a depositForBurnWithHook transaction with the Forwarding Service hook data. The hook data tells the CCTP Forwarding Service to automatically forward the mint transaction on the destination chain. The Forwarding Service hook data is a static 32-byte value containing the magic bytes cctp-forward, version 0, and length 0. For details on the hook format, see Forwarding Service hook format.
// Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0)
const FORWARDING_SERVICE_HOOK_DATA =
  "0x636374702d666f72776172640000000000000000000000000000000000000000";
Then, send the depositForBurnWithHook transaction:
import { pad, encodeFunctionData } from "viem";

// Configuration
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS";

// Convert address to bytes32 format
const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 });

async function depositForBurnWithHook() {
  console.log("Burning USDC on Base with Forwarding Service hook...");

  const burnTx = await client.sendTransaction({
    to: BASE_SEPOLIA_TOKEN_MESSENGER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "depositForBurnWithHook",
          stateMutability: "nonpayable",
          inputs: [
            { name: "amount", type: "uint256" },
            { name: "destinationDomain", type: "uint32" },
            { name: "mintRecipient", type: "bytes32" },
            { name: "burnToken", type: "address" },
            { name: "destinationCaller", type: "bytes32" },
            { name: "maxFee", type: "uint256" },
            { name: "minFinalityThreshold", type: "uint32" },
            { name: "hookData", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "depositForBurnWithHook",
      args: [
        transferAmount, // amount to transfer
        AVALANCHE_FUJI_DOMAIN, // destination domain
        mintRecipientBytes32, // recipient address as bytes32
        BASE_SEPOLIA_USDC, // burn token (USDC on Base)
        pad("0x0000000000000000000000000000000000000000", { size: 32 }), // destinationCaller (0x0 = any caller)
        maxFee, // max fee from step 2
        1000, // minFinalityThreshold (1000 for Fast Transfer)
        FORWARDING_SERVICE_HOOK_DATA, // Forwarding Service hook data
      ],
    }),
  });
  console.log("Burn Tx:", burnTx);
  return burnTx;
}
Once the burn transaction is confirmed on Base, the Circle Forwarding Service automatically handles the attestation and mint transaction on Avalanche. The USDC is minted directly to the mintRecipient address on the destination chain.

Full example code

The following is a complete example of how to transfer USDC from Base Sepolia to Avalanche Fuji using the Forwarding Service.
import { createWalletClient, http, encodeFunctionData, pad } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// -------- Configuration --------
const CONFIG = {
  PRIVATE_KEY: process.env.PRIVATE_KEY,

  // Contracts (Base Sepolia testnet)
  BASE_SEPOLIA_USDC: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  BASE_SEPOLIA_TOKEN_MESSENGER: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",

  // Transfer parameters
  TRANSFER_AMOUNT_USDC: "10", // 10 USDC
  DESTINATION_ADDRESS: process.env.DESTINATION_ADDRESS,

  // Domain IDs
  BASE_DOMAIN: 6,
  AVALANCHE_DOMAIN: 1,
};

// Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0)
const FORWARDING_SERVICE_HOOK_DATA =
  "0x636374702d666f72776172640000000000000000000000000000000000000000";

// -------- Helper functions --------
function toUSDC(amount) {
  return BigInt(Math.floor(parseFloat(amount) * 1_000_000));
}

// -------- Main function --------
async function main() {
  const {
    PRIVATE_KEY,
    BASE_SEPOLIA_USDC,
    BASE_SEPOLIA_TOKEN_MESSENGER,
    TRANSFER_AMOUNT_USDC,
    DESTINATION_ADDRESS,
    BASE_DOMAIN,
    AVALANCHE_DOMAIN,
  } = CONFIG;

  // Set up wallet client
  const account = privateKeyToAccount(PRIVATE_KEY);
  const client = createWalletClient({
    chain: baseSepolia,
    transport: http(),
    account,
  });

  console.log("Wallet address:", account.address);
  console.log("Destination address:", DESTINATION_ADDRESS);
  console.log("Transfer amount:", TRANSFER_AMOUNT_USDC, "USDC");

  // Step 1: Get fees from API
  console.log("\nStep 1: Getting CCTP fees...");
  const feeResponse = await fetch(
    `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${BASE_DOMAIN}/${AVALANCHE_DOMAIN}?forward=true`,
    {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    },
  );
  const fees = await feeResponse.json();
  console.log("Fees:", JSON.stringify(fees, null, 2));

  // Step 2: Calculate amounts
  console.log("\nStep 2: Calculating amounts...");
  const transferAmount = toUSDC(TRANSFER_AMOUNT_USDC);
  const feeData = fees[0]; // Fast transfer
  const forwardFee = BigInt(feeData.forwardFee.med);
  const protocolFeeBps = BigInt(feeData.minimumFee);
  const protocolFee = (transferAmount * protocolFeeBps) / 10000n;
  const maxFee = forwardFee + protocolFee;

  console.log("Transfer amount:", transferAmount.toString(), "minor units");
  console.log("Max fee:", maxFee.toString(), "minor units");

  // Step 3: Approve USDC
  console.log("\nStep 3: Approving USDC transfer...");
  const approveTx = await client.sendTransaction({
    to: BASE_SEPOLIA_USDC,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "approve",
          stateMutability: "nonpayable",
          inputs: [
            { name: "spender", type: "address" },
            { name: "amount", type: "uint256" },
          ],
          outputs: [{ name: "", type: "bool" }],
        },
      ],
      functionName: "approve",
      args: [BASE_SEPOLIA_TOKEN_MESSENGER, transferAmount],
    }),
  });
  console.log("Approval Tx:", approveTx);

  // Step 4: Burn USDC with Forwarding Service hook
  console.log("\nStep 4: Burning USDC with Forwarding Service hook...");
  const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 });
  const zeroBytes32 = pad("0x0000000000000000000000000000000000000000", {
    size: 32,
  });

  const burnTx = await client.sendTransaction({
    to: BASE_SEPOLIA_TOKEN_MESSENGER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "depositForBurnWithHook",
          stateMutability: "nonpayable",
          inputs: [
            { name: "amount", type: "uint256" },
            { name: "destinationDomain", type: "uint32" },
            { name: "mintRecipient", type: "bytes32" },
            { name: "burnToken", type: "address" },
            { name: "destinationCaller", type: "bytes32" },
            { name: "maxFee", type: "uint256" },
            { name: "minFinalityThreshold", type: "uint32" },
            { name: "hookData", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "depositForBurnWithHook",
      args: [
        transferAmount,
        AVALANCHE_DOMAIN,
        mintRecipientBytes32,
        BASE_SEPOLIA_USDC,
        zeroBytes32,
        maxFee,
        1000, // Fast Transfer
        FORWARDING_SERVICE_HOOK_DATA,
      ],
    }),
  });
  console.log("Burn Tx:", burnTx);

  console.log(
    "\nTransfer initiated! The Forwarding Service will automatically mint USDC on Avalanche.",
  );
}

main().catch(console.error);