> ## 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.

# How-to: Transfer USDC with the Forwarding Service

> Transfer USDC Crosschain with the Circle Forwarding Service

This guide shows how to transfer USDC crosschain using the
[Circle Forwarding Service](/cctp/concepts/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 blockchain](/cctp/concepts/supported-chains-and-domains), including
Solana.

When you use the Forwarding Service, Circle handles the mint transaction on the
destination blockchain, eliminating the need for you to hold native tokens for
gas on the destination blockchain or run multichain infrastructure.

## Prerequisites

Before you start, ensure you have:

* Installed [Node.js v22+](https://nodejs.org/)
* Created a TypeScript project and installed the `viem` package.
* 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 Sepolia to
Avalanche Fuji. This value is passed to the `maxFee` parameter in the
`depositForBurnWithHook` transaction. The following is an example request using
source domain 6 (Base Sepolia) and destination domain 1 (Avalanche Fuji):

```typescript theme={null}
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);
```

**Example response:**

```json theme={null}
[
  {
    "finalityThreshold": 1000, // Fast transfer
    "minimumFee": 1.3, // Basis points (0.013% fee rate)
    "forwardFee": {
      // Gas-based, fluctuates based on destination chain gas prices
      "low": 206035, // 0.206035 USDC
      "med": 207543, // 0.207543 USDC
      "high": 209052 // 0.209052 USDC
    }
  },
  {
    "finalityThreshold": 2000, // Standard transfer
    "minimumFee": 0, // No fee
    "forwardFee": {
      "low": 206035, // 0.206035 USDC
      "med": 207543, // 0.207543 USDC
      "high": 209052 // 0.209052 USDC
    }
  }
]
```

The `forwardFee` is the fee charged by the Forwarding Service. The `minimumFee`
is the CCTP protocol fee rate in basis points, applied as a percentage of the
transfer amount.

<Tip>
  Circle recommends selecting the `med` fee level or higher from the `forwardFee`
  object in the API response. Note that `forwardFee` values fluctuate based on
  destination chain gas prices. Make the query immediately before initiating your
  transfer.
</Tip>

<Accordion title="Forwarding to Solana">
  When the destination blockchain is Solana, the `forwardFee` values include both
  gas and rent costs. If the recipient does not have an existing USDC
  [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account),
  add `includeRecipientSetup=true` to the fee query so the returned fee covers ATA
  creation:

  ```typescript theme={null}
  // Source domain 6 (Base Sepolia), destination domain 5 (Solana)
  const response = await fetch(
    "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/5?forward=true&includeRecipientSetup=true",
    {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    },
  );
  ```

  Unlike EVM destinations, the `mintRecipient` for Solana must be the recipient's
  **USDC token account address** (ATA), not the wallet address. Derive the ATA
  from the wallet address and the USDC mint:

  ```typescript theme={null}
  import { getAssociatedTokenAddressSync } from "@solana/spl-token";
  import { PublicKey } from "@solana/web3.js";

  const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
  const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet

  const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet);
  const mintRecipientBytes32 =
    `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}` as `0x${string}`;
  ```

  If the recipient does not have an existing ATA and you passed
  `includeRecipientSetup=true` in the fee query, you must also encode ATA creation
  fields in the hook data. See
  [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation)
  for the extended hook data format.
</Accordion>

### 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.

```typescript theme={null}
// Amount to transfer (10 USDC in subunits)
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);

// Calculate protocol fee (minimumFee is in basis points)
const minimumFeeBps = feeData.minimumFee;
const protocolFee =
  (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;

// Total max fee should cover both fees
const maxFee = forwardFee + protocolFee;
const totalAmount = transferAmount + maxFee; // Total to burn

console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
```

In this example, for a 10 USDC transfer with forwarding, the total fee is
0.208843 USDC (0.207543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol
fee). For the recipient to receive 10 USDC, you must burn 10.208843 USDC in
total.

<Warning>
  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.
</Warning>

### Step 3. Approve the USDC transfer

Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on
Base to transfer USDC from your wallet. Approve at least `totalAmount`
(including fees) calculated in Step 2.

```typescript theme={null}
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 as `0x${string}`);
const client = createWalletClient({
  chain: baseSepolia,
  transport: http(),
  account,
});

async function approveUSDC(amount: bigint) {
  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;
}

// Approve the total amount (from Step 2)
await approveUSDC(totalAmount);
```

### 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](/cctp/concepts/forwarding-service#hook-format).

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

<Accordion title="Hook data for Solana destinations">
  When forwarding to Solana, use the same static hook data if the recipient
  already has a USDC
  [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account).
  If the recipient does not have an ATA and you included
  `includeRecipientSetup=true` in the fee query (see Step 1), construct extended
  hook data that requests ATA creation:

  ```typescript theme={null}
  import { PublicKey } from "@solana/web3.js";

  // Magic bytes "cctp-forward" padded to 24 bytes
  const magicBytes = Buffer.alloc(24);
  magicBytes.write("cctp-forward", "utf-8");

  // Version (uint32, big-endian) = 0
  const version = Buffer.alloc(4);

  // Length of additional Circle hook data (uint32, big-endian) = 33
  const length = Buffer.alloc(4);
  length.writeUInt32BE(33);

  // ATA creation flag = 1
  const ataFlag = Buffer.from([1]);

  // Recipient wallet address (32 bytes)
  const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
  const walletBytes = Buffer.from(recipientWallet.toBytes());

  const FORWARDING_SERVICE_HOOK_DATA =
    ("0x" +
      Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString(
        "hex",
      )) as `0x${string}`;
  ```

  For the full hook data format, see
  [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation).
</Accordion>

Then, send the `depositForBurnWithHook` transaction:

<Note>
  Use `totalAmount` (transfer amount + fees) for the `amount` parameter. The
  recipient receives only the transfer amount after fees are deducted.
</Note>

```typescript theme={null}
import { pad, encodeFunctionData } from "viem";

// Configuration
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS" as `0x${string}`;

// 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: [
        totalAmount, // Total to burn (recipient receives transferAmount after fees)
        AVALANCHE_FUJI_DOMAIN,
        mintRecipientBytes32,
        BASE_SEPOLIA_USDC,
        pad("0x", { size: 32 }), // destinationCaller (empty = any caller)
        maxFee,
        1000,
        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.
The recipient receives `transferAmount` USDC (fees are automatically deducted
from the `totalAmount` on the destination chain).

### Step 5. Verify the mint transaction

After the burn transaction is confirmed, query the Circle Iris API to retrieve
the forwarding details. The API returns the `forwardTxHash`, which is the mint
transaction hash on the destination chain.

The attestation may take time to become available, depending on the destination
chain. Poll the API until the message is ready:

```typescript theme={null}
// Configuration
const BASE_SEPOLIA_DOMAIN = 6;

process.stdout.write("Waiting for attestation...");

let mintTx;
while (!mintTx) {
  const messageResponse = await fetch(
    `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
  );
  const data = await messageResponse.json();

  if (data.messages?.[0]?.forwardTxHash) {
    mintTx = data.messages[0].forwardTxHash;
    console.log(); // New line after dots
  } else {
    process.stdout.write(".");
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}

console.log("Mint Tx:", mintTx);
```

## Full example code

The following is a complete example of how to transfer USDC from Base Sepolia to
Avalanche Fuji using the Forwarding Service. Remember to set the `PRIVATE_KEY`
and `DESTINATION_ADDRESS` environment variables.

```typescript script.ts expandable theme={null}
import { createWalletClient, http, encodeFunctionData, pad } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// Validate environment variables
if (!process.env.PRIVATE_KEY || !process.env.DESTINATION_ADDRESS) {
  throw new Error(
    "PRIVATE_KEY and DESTINATION_ADDRESS environment variables are required",
  );
}

// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
  "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const BASE_SEPOLIA_DOMAIN = 6;
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = process.env.DESTINATION_ADDRESS as `0x${string}`;

// Forwarding Service hook data
const FORWARDING_SERVICE_HOOK_DATA =
  "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`;

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

async function main() {
  console.log("Wallet address:", account.address);
  console.log("Destination address:", DESTINATION_ADDRESS);

  // 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_SEPOLIA_DOMAIN}/${AVALANCHE_FUJI_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 = 10_000_000n; // 10 USDC
  const feeData = fees[0]; // Fast transfer
  const forwardFee = BigInt(feeData.forwardFee.med);
  const minimumFeeBps = feeData.minimumFee;
  const protocolFee =
    (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;
  const maxFee = forwardFee + protocolFee;
  const totalAmount = transferAmount + maxFee; // Total to burn

  console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
  console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
  console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
  console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
  console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");

  // 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, totalAmount],
    }),
  });
  console.log("Approval Tx:", approveTx);

  // Step 4: Burn USDC with Forwarding Service hook
  console.log("\nStep 4: Burning USDC 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: [
        totalAmount,
        AVALANCHE_FUJI_DOMAIN,
        pad(DESTINATION_ADDRESS as `0x${string}`, { size: 32 }),
        BASE_SEPOLIA_USDC,
        pad("0x", { size: 32 }),
        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.",
  );

  // Step 5: Verify the mint transaction
  console.log("\nStep 5: Verifying mint transaction...");
  process.stdout.write("Waiting for attestation...");

  let mintTx;
  while (!mintTx) {
    const messageResponse = await fetch(
      `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
    );
    const data = await messageResponse.json();

    if (data.messages?.[0]?.forwardTxHash) {
      mintTx = data.messages[0].forwardTxHash;
      console.log("\n"); // New line after dots
    } else {
      process.stdout.write(".");
      await new Promise((resolve) => setTimeout(resolve, 2000));
    }
  }

  console.log("Mint Tx:", mintTx);
}

main().catch(console.error);
```

Run the script:

```bash theme={null}
npx tsx script.ts
```
