Skip to main content
This guide demonstrates how to transfer USDC from Ethereum Sepolia to Arc testnet using CCTP. You use the viem framework to interact with CCTP contracts and the CCTP API to retrieve attestations.
Use Bridge Kit to simplify crosschain transfers with CCTP.This quickstart shows how to transfer USDC from to using a manual CCTP integration. The example is for learning or for developers who need a manual integration.To streamline this, use Bridge Kit to transfer USDC in just a few lines of code.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22.6+
  • Prepared an EVM testnet wallet with the private key available
  • Funded your wallet with the following testnet tokens:
    • Sepolia ETH (native token) from a public faucet
    • Sepolia USDC from the Circle Faucet
    • Arc testnet USDC from the Circle Faucet if you choose the direct mint path below, because the destination wallet must pay gas to call receiveMessage

Step 1. Set up the project

1.1. Create the project and install dependencies

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
Then, update the tsconfig.json file:

1.3. Set environment variables

Open .env in your editor and add:
  • PRIVATE_KEY is the private key for the Ethereum Sepolia EOA that signs the source-chain approval and burn transactions. The direct-mint path also uses the same key to submit the destination mint on Arc.
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.
The npm run start command loads variables from .env using Node.js native env-file support.
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: Configure the script

This section covers the necessary setup for the transfer script, including defining keys and addresses, and configuring the wallet client for interacting with the source and destination chains.

2.1. Define configuration constants

The script predefines the contract addresses, transfer amount, and maximum fee. Update the DESTINATION_ADDRESS with your wallet address. For simplicity, this quickstart uses the same EOA as the Ethereum Sepolia source signer and the Arc recipient. In production, these can be different addresses.
TypeScript

2.2. Set up wallet clients

The wallet client configures the appropriate network settings using viem. The direct-mint path below uses clients for both Ethereum Sepolia and Arc testnet. The Forwarding Service path only needs the source-chain client on Ethereum Sepolia.
TypeScript

Step 3: Implement the transfer logic

The following sections outline the core transfer logic. The path diverges at the source-chain burn transaction:
  • Direct mint uses depositForBurn, then retrieves an attestation and calls receiveMessage on Arc.
  • Forwarding Service uses depositForBurnWithHook, then lets Circle handle the destination-side mint on Arc.

3.1. Get forwarding fees and calculate the burn amount

Before you burn USDC with the Forwarding Service, query the CCTP fee endpoint with forward=true. The forwarding fee is dynamic, so fetch it immediately before the transfer. The returned maxFee must cover both the CCTP protocol fee and the forwarding fee.
TypeScript

3.2. Approve the total burn amount

Approve the total amount you will burn on the source chain. For the forwarding path, that is the transfer amount plus the forwarding and protocol fees.
TypeScript

3.3. Burn USDC with the Forwarding Service hook

Use depositForBurnWithHook on the source chain. The forwarding hook data tells Circle to handle the destination-side receiveMessage call on Arc.
TypeScript

3.4. Verify the forwarded mint

After the burn is confirmed, poll the Iris API until it returns a forwardTxHash. That hash is the Arc destination mint transaction submitted by Circle. In the forwarding path, forwardTxHash is the completion signal for the destination-side mint. You do not need to retrieve an attestation and call receiveMessage yourself.
TypeScript

Step 4: Complete script

Create a index.ts file in your project directory and populate it with the complete code below for the path you want to test.
index.ts

Step 5: Test the script

Run the following command to execute the script:
Shell
Once the script runs and the transfer is finalized, a confirmation receipt is logged in the console.
Rate limit: The attestation service rate limit is 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five minutes and returns an HTTP 429 (Too Many Requests) response.