Skip to main content
Cross-Chain Transfer Protocol (CCTP) is a permissionless onchain utility that facilitates USDC transfers securely between supported blockchains via native burning and minting. For more info, visit the CCTP docs.
In this tutorial, you’ll use Circle’s Bridge Kit to programmatically transfer USDC from an EVM chain (for example, Ethereum Sepolia) or Solana Devnet to Arc Testnet with Circle Dev-Controlled Wallets.

Prerequisites

Before you begin, make sure you have:
  1. Installed Node.js v22+ and npm.
  2. A Circle Developer Console account
  3. An API key created in the Console:
    Keys → Create a key → API key → Standard Key
  4. Your Entity Secret registered

Step 1: Set up your project

In this step, you prepare your project and environment.

1.1. Create a new project

Create a new directory, navigate to it and initialize a new project.
mkdir crosschain-transfer
cd crosschain-transfer
npm init -y
npm pkg set type=module
Install Bridge Kit, Circle Wallets adapter and supporting tools.
npm install @circle-fin/bridge-kit @circle-fin/adapter-circle-wallets typescript tsx

1.2. Initialize and configure the project

This command creates a tsconfig.json file:
# Initialize a TypeScript project
npx tsc --init
Then, edit the tsconfig.json file:
# Replace the contents of the generated file
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true
  }
}
EOF

1.3 Configure environment variables

Create a .env file in the project directory with your Circle credentials, replacing these placeholders with your own credentials:
  • CIRCLE_API_KEY: your API key should be either environment-prefixed (for example, TEST_API_KEY:abc123:def456 or LIVE_API_KEY:xyz:uvw) or base64-encoded strings.
  • CIRCLE_ENTITY_SECRET: your entity secret should be 64 lowercase alphanumeric characters.
echo "CIRCLE_ API_KEY={YOUR_API_KEY}
CIRCLE_ ENTITY_SECRET={YOUR_ENTITY_SECRET}" > .env
Important: These are sensitive credentials. Do not commit them to version control or share them publicly.

Step 2: Set up your wallets

In this step, you create two dev-controlled wallets and fund one of them with testnet tokens to make the transfer. If you already have funded dev-controlled wallets, skip to Step 3.

2.1. Create wallets

Install the Circle Wallets SDK. It is also possible to call the API directly if you can’t use the SDK in your project.
npm install @circle-fin/developer-controlled-wallets
Import the Wallets SDK and initialize the client using your API key and Entity Secret. Dev-controlled wallets are created in a wallet set, which serves as the source from which individual wallet keys are derived.
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

const client = initiateDeveloperControlledWalletsClient({
  apiKey: "<YOUR_API_KEY>",
  entitySecret: "<YOUR_ENTITY_SECRET>",
});

// Create a wallet set
const walletSetResponse = await client.createWalletSet({
  name: "Wallet Set 1",
});

// Create a wallet on Arc Testnet
const walletsResponse = await client.createWallets({
  blockchains: ["ARC-TESTNET", "SOL-DEVNET"],
  count: 1,
  walletSetId: walletSetResponse.data?.walletSet?.id ?? "",
});
If you are calling the API directly, you’ll need to make two requests: one to create the wallet set; one to create the wallet.Be sure to replace the Entity Secret ciphertext and the idempotency key in your request. If you are using the SDKs, this is automatically taken care of for you.
You should now have two new Externally Owned Account (EOA) developer-controlled wallets that you can also see from the Circle Developer Console. The API response will look similar to the following:
[
  {
    id: "cd8a3a9a-ed17-5e9b-8f6b-7b01a0f27478",
    state: "LIVE",
    walletSetId: "1bb349fd-99dd-50a1-999b-05901533a98d",
    custodyType: "DEVELOPER",
    address: "0xb0b70e1d3599e84fcd61915c023ea12a843506b0",
    blockchain: "ARC-TESTNET",
    accountType: "EOA",
    updateDate: "2026-01-07T06:45:43Z",
    createDate: "2026-01-07T06:45:43Z",
  }, {
    id: "1557b6cf-3d54-5738-84ec-1ceba833e27b",
    state: "LIVE",
    walletSetId: "1bb349fd-99dd-50a1-999b-05901533a98d",
    custodyType: "DEVELOPER",
    address: "DGJ1oZtR7Nk1AidTeKYs1b557dJHHXKBNdVDem7uHDN8",
    blockchain: "SOL-DEVNET",
    accountType: "EOA",
    updateDate: "2026-01-07T06:45:43Z",
    createDate: "2026-01-07T06:45:43Z",
  }
]

2.2. Fund a wallet with testnet tokens

Obtain testnet USDC or EURC from the Circle Faucet and native tokens from the Console Faucet to pay gas fees. You’ll need a funded balance to execute transactions using your dev-controlled wallet.

2.3. Check the wallet’s balance

You can check your wallets’ balance from the Developer Console or programmatically by making a request to GET /wallets/{id}/balances with the specified wallet ID.
const response = await client.getWalletTokenBalance({
  id: "<WALLET_ID>",
});

Step 3: Bridge USDC

In this step, you set up your script, execute the bridge transfer, and check the result.

3.1. Create the script

Create an index.ts file in the project directory and add the following code. This code sets up your script and transfers 1 USDC from your chosen chain to Arc Testnet. It also listens to bridge events that occur during the transfer lifecycle.
// Import Bridge Kit and its dependencies
import { BridgeKit } from "@circle-fin/bridge-kit";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";
import { inspect } from "util";

// Initialize the SDK and listen to events
const kit = new BridgeKit();
kit.on("*", (payload) => {
  console.log("Event received:", payload);
});

const bridgeUSDC = async (): Promise<void> => {
  try {
    // Set up the Circle Wallets adapter instance, works for both ecosystems
    const adapter = createCircleWalletsAdapter({
      apiKey: process.env.CIRCLE_API_KEY!,
      entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
    });

    console.log("---------------Starting Bridging---------------");

    // Use the same adapter for the source and destination blockchains
    const result = await kit.bridge({
      from: { adapter, chain: "Arbitrum_Sepolia" },
      to: { adapter, chain: "Arc_Testnet" },
      amount: "1.00",
    });

    console.log("RESULT", inspect(result, false, null, true));
  } catch (err) {
    console.log("ERROR", inspect(err, false, null, true));
  }
};

void bridgeUSDC();

3.2. Run the script

Save the index.ts file and run the script in your terminal:
npx tsx --env-file=.env index.ts
For blockchains other than Arc, you will need native tokens to pay for gas. The approve and burn steps require gas fees in the native token of the source chain, while the mint step requires gas fees in the native token of the destination chain.

3.3. Verify the transfer

After the script finishes, locate the returned steps array in the terminal output. Each transaction step includes an explorerUrl field. Use that URL to verify that the USDC amount matches the amount you transferred. The following example shows how all four steps (Approve, Burn, Fetch Attestation, Mint) might look like in the terminal output. The values shown are for demonstration purposes only and don’t represent a real transaction:
{
  name: 'approve',
  state: 'success',
  txHash: '0x809cd6678785c3cb48d73a8e35e5ef8fc21c1b1e22df767860bc30bd882fb470',
  data: {
    txHash: '0x809cd6678785c3cb48d73a8e35e5ef8fc21c1b1e22df767860bc30bd882fb470',
    status: 'success',
    cumulativeGasUsed: 429183n,
    gasUsed: 38596n,
    blockNumber: 20523074n,
    blockHash: '0x5d81b5abab77f19fb3e70df620276fe9b4be68172482afa0188296ead97c3033',
    transactionIndex: 5,
    effectiveGasPrice: 162000000000n
  },
  explorerUrl: 'https://testnet.arcscan.app/tx/0x809cd6678785c3cb48d73a8e35e5ef8fc21c1b1e22df767860bc30bd882fb470'
}

Summary

After completing this tutorial, you’ve successfully:
  • Created dev-controlled wallets
  • Funded your wallet with testnet USDC
  • Bridged USDC from one chain to another