Skip to main content
This guide walks you through the process of creating Unified Crosschain USDC Balances on supported EVM chains using Circle Gateway, and performing transfers from EVM to EVM and from Solana to EVM.
Use Unified Balance Kit to simplify this integration.This quickstart uses a manual Gateway integration. It is for learning or for developers who need direct control.To simplify, use Unified Balance Kit to deposit and spend USDC in just a few lines of code.
Select a tab below for the Circle Wallets or self-managed wallet path.

Prerequisites

This quickstart uses Arc Testnet for deposits and the Arc Testnet -> Base Sepolia route for the EVM transfer example. You can adapt the same code to other supported EVM testnets later in the guide.Before you begin, ensure that you’ve:If you want to try the Solana to EVM transfer, ensure that you’ve:If you want to use the Direct Mint path for Solana to EVM, also create an EVM Developer-Controlled Wallet on the destination chain to submit the mint transaction.

Add testnet funds to your wallet

To interact with Gateway, you need test USDC and native tokens in your wallet on each chain you deposit from. In the default Arc Testnet deposit flow, the same USDC also covers transaction fees because Arc uses USDC as the native gas token.Use the Circle Faucet to get test USDC. If you have a Circle Developer Console account, you can use the Console Faucet to get testnet native tokens. In addition, the following faucets can also be used to fund your wallet with testnet native tokens:
Faucet: Arc Testnet (USDC + native tokens)
PropertyValue
Chain namearcTestnet
USDC address0x3600000000000000000000000000000000000000
Domain ID26

Step 1. Set up your project

1.1. Create the project and install dependencies

# Set up your directory and initialize a Node.js project
mkdir unified-gateway-balance-evm-circle-wallets
cd unified-gateway-balance-evm-circle-wallets
npm init -y

# Set up module type and run scripts
npm pkg set type=module
npm pkg set scripts.deposit="tsx --env-file=.env deposit.ts"
npm pkg set scripts.balances="tsx --env-file=.env balances.ts"
npm pkg set scripts.transfer-from-evm="tsx --env-file=.env transfer-from-evm.ts"

# Install runtime dependencies
npm install @circle-fin/developer-controlled-wallets tsx typescript

# Install dev dependencies
npm install --save-dev @types/node
If you want to try the Solana to EVM transfer, add the run script and install these additional dependencies:
npm pkg set scripts.transfer-from-sol="tsx --env-file=.env transfer-from-sol.ts"
npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
npm install @solana/buffer-layout @solana/web3.js bs58

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
npx tsc --init
Then, update the tsconfig.json file:
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.3. Set environment variables

Create a .env file in the project directory:
.env
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
DEPOSITOR_ADDRESS=YOUR_SOURCE_WALLET_ADDRESS
RECIPIENT_ADDRESS=YOUR_DESTINATION_WALLET_ADDRESS
  • CIRCLE_API_KEY is your Circle API key.
  • CIRCLE_ENTITY_SECRET is your Circle entity secret.
  • DEPOSITOR_ADDRESS is the source depositor wallet for the script you are running.
  • RECIPIENT_ADDRESS is the destination wallet. It is only required for transfer scripts.
For transfer-from-evm.ts, both DEPOSITOR_ADDRESS and RECIPIENT_ADDRESS are EVM addresses.For transfer-from-sol.ts, DEPOSITOR_ADDRESS is a Solana address and RECIPIENT_ADDRESS is an EVM address.
For the Circle Wallets Direct Mint path, RECIPIENT_ADDRESS must be a destination-chain Developer-Controlled Wallet address in your Circle account. The script uses createContractExecutionTransaction(...) to submit gatewayMint(...) from that wallet, so a regular external EVM address will not work.
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.

Step 2. Set up the configuration file

The deposit and transfer scripts share this configuration file.

2.1. Create the configuration file

touch config.ts

2.2. Configure shared Gateway constants

Add the shared Gateway constants used by the deposit and transfer scripts. To swap chains later, keep chainConfig as the shared map and update the SOURCE_CHAIN and DEST_CHAIN constants inside each script.
config.ts
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

type WalletChain = "ARC-TESTNET" | "BASE-SEPOLIA";

type EvmChainConfig = {
  chainName: string;
  usdc: string;
  domain: number;
  walletChain: WalletChain;
};

export const chainConfig = {
  arc: {
    chainName: "Arc Testnet",
    usdc: "0x3600000000000000000000000000000000000000",
    domain: 26,
    walletChain: "ARC-TESTNET",
  },
  base: {
    chainName: "Base Sepolia",
    usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    domain: 6,
    walletChain: "BASE-SEPOLIA",
  },
} as const satisfies Record<string, EvmChainConfig>;

export type ChainKey = keyof typeof chainConfig;

export const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
export const GATEWAY_WALLET_ADDRESS =
  "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
export const GATEWAY_MINTER_ADDRESS =
  "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";

export const API_KEY = process.env.CIRCLE_API_KEY!;
export const ENTITY_SECRET = process.env.CIRCLE_ENTITY_SECRET!;
export const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;

if (!API_KEY || !ENTITY_SECRET || !DEPOSITOR_ADDRESS) {
  console.error(
    "Missing required env vars: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, DEPOSITOR_ADDRESS",
  );
  process.exit(1);
}

/* Circle Wallets Client */
export const client = initiateDeveloperControlledWalletsClient({
  apiKey: API_KEY,
  entitySecret: ENTITY_SECRET,
});

export async function waitForTxCompletion(
  client: ReturnType<typeof initiateDeveloperControlledWalletsClient>,
  txId: string,
  label: string,
) {
  const terminalStates = new Set([
    "COMPLETE",
    "CONFIRMED",
    "FAILED",
    "DENIED",
    "CANCELLED",
  ]);

  process.stdout.write(`Waiting for ${label} (txId=${txId})\n`);

  while (true) {
    const { data } = await client.getTransaction({ id: txId });
    const state = data?.transaction?.state;

    process.stdout.write(".");

    if (state && terminalStates.has(state)) {
      process.stdout.write("\n");
      console.log(`${label} final state: ${state}`);

      if (state !== "COMPLETE" && state !== "CONFIRMED") {
        throw new Error(
          `${label} did not complete successfully (state=${state})`,
        );
      }
      return data.transaction;
    }
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
}

export function parseBalance(
  value: string | number | null | undefined,
): bigint {
  const str = String(value ?? "0");
  const [whole, decimal = ""] = str.split(".");
  const decimal6 = (decimal + "000000").slice(0, 6);
  return BigInt((whole || "0") + decimal6);
}

export function addressToBytes32(address: string) {
  return ("0x" +
    address
      .toLowerCase()
      .replace(/^0x/, "")
      .padStart(64, "0")) as `0x${string}`;
}

export function stringifyTypedData<T>(obj: T) {
  return JSON.stringify(obj, (_key, value) =>
    typeof value === "bigint" ? value.toString() : value,
  );
}

Step 3. Deposit into a unified crosschain balance (Circle Wallets)

The deposit script deposits USDC into the Gateway Wallet on Arc Testnet. You can skip to the full deposit script if you prefer.
Do not transfer USDC directly to the Gateway Wallet contract with a standard ERC-20 transfer. You must call a Gateway deposit method or the USDC will not be credited to your unified balance.

3.1. Create the deposit script

touch deposit.ts

3.2. Define constants and deposit amount

Set the deposit amount near the top of the script. This example uses Arc Testnet as the source chain, and you can swap chains later by changing SOURCE_CHAIN.
const DEPOSIT_AMOUNT_USDC = "2";

3.3. Approve USDC spending and submit the deposit

The script first approves the Gateway Wallet contract to spend USDC, then calls the Gateway deposit(address,uint256) method on the configured source chain.
const SOURCE_CHAIN = "arc" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];

const approveTx = await client.createContractExecutionTransaction({
  walletAddress: DEPOSITOR_ADDRESS,
  blockchain: sourceConfig.walletChain,
  contractAddress: sourceConfig.usdc,
  abiFunctionSignature: "approve(address,uint256)",
  abiParameters: [
    GATEWAY_WALLET_ADDRESS,
    parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
  ],
  fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});

const depositTx = await client.createContractExecutionTransaction({
  walletAddress: DEPOSITOR_ADDRESS,
  blockchain: sourceConfig.walletChain,
  contractAddress: GATEWAY_WALLET_ADDRESS,
  abiFunctionSignature: "deposit(address,uint256)",
  abiParameters: [
    sourceConfig.usdc,
    parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
  ],
  fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});

3.4. Wait for Circle Wallet transaction to complete

Circle Wallet contract execution is asynchronous. After each submit step, wait for transaction completion before proceeding to the next phase.
await waitForTxCompletion(client, approveTxId, "USDC approve");
await waitForTxCompletion(client, depositTxId, "Gateway deposit");

3.5. Full deposit script (Circle Wallets)

The script approves USDC spending and deposits into the Gateway Wallet on the configured source chain. Inline comments explain each stage.
deposit.ts
import {
  chainConfig,
  GATEWAY_WALLET_ADDRESS,
  DEPOSITOR_ADDRESS,
  client,
  waitForTxCompletion,
  parseBalance,
} from "./config.js";

const DEPOSIT_AMOUNT_USDC = "2";
const SOURCE_CHAIN = "arc" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];

async function main() {
  console.log(`Using account: ${DEPOSITOR_ADDRESS}`);
  console.log(`Depositing on: ${sourceConfig.chainName}`);

  // [1] Approve the Gateway Wallet to spend USDC on the source chain.
  console.log(
    `Approving ${DEPOSIT_AMOUNT_USDC} USDC on ${sourceConfig.chainName}...`,
  );

  const approveTx = await client.createContractExecutionTransaction({
    walletAddress: DEPOSITOR_ADDRESS,
    blockchain: sourceConfig.walletChain,
    contractAddress: sourceConfig.usdc,
    abiFunctionSignature: "approve(address,uint256)",
    abiParameters: [
      GATEWAY_WALLET_ADDRESS,
      parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
    ],
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });

  const approveTxId = approveTx.data?.id;
  if (!approveTxId) throw new Error("Failed to create approve transaction");
  await waitForTxCompletion(client, approveTxId, "USDC approve");

  // [2] Call the Gateway deposit function on the source chain.
  console.log(`Depositing ${DEPOSIT_AMOUNT_USDC} USDC to Gateway Wallet`);

  const depositTx = await client.createContractExecutionTransaction({
    walletAddress: DEPOSITOR_ADDRESS,
    blockchain: sourceConfig.walletChain,
    contractAddress: GATEWAY_WALLET_ADDRESS,
    abiFunctionSignature: "deposit(address,uint256)",
    abiParameters: [
      sourceConfig.usdc,
      parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
    ],
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });

  const depositTxId = depositTx.data?.id;
  if (!depositTxId) throw new Error("Failed to create deposit transaction");
  await waitForTxCompletion(client, depositTxId, "Gateway deposit");

  console.log(
    "\n==| Block confirmation may take up to 19 minutes for some chains |==",
  );
}

main().catch((error) => {
  console.error("\nError:", error);
  process.exit(1);
});

3.6. Run the deposit script

Run the script:
npm run deposit
Wait for the required number of block confirmations. Once the deposit transactions are final, the total balance is the sum of all the USDC from deposit transactions across all supported chains that have reached finality.If you later adapt this deposit script to another EVM chain, update SOURCE_CHAIN to another supported entry in chainConfig.

3.7. Check the balances on the Gateway Wallet

Create a new file called balances.ts, and add the following code. This script retrieves the USDC balances available from your Gateway Wallet for the DEPOSITOR_ADDRESS currently set in .env.
balances.ts
interface GatewayBalancesResponse {
  balances: Array<{
    domain: number;
    balance: string;
  }>;
}

const EVM_DOMAINS = {
  base: 6,
  arc: 26,
};

const SOLANA_DOMAINS = {
  solana: 5,
};

const DOMAINS = { ...EVM_DOMAINS, ...SOLANA_DOMAINS };

const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;

if (!DEPOSITOR_ADDRESS) {
  console.error("Missing required env var: DEPOSITOR_ADDRESS");
  process.exit(1);
}

const isEvmAddress = DEPOSITOR_ADDRESS.startsWith("0x");

async function main() {
  console.log(`Depositor address: ${DEPOSITOR_ADDRESS}`);
  console.log(`Address type: ${isEvmAddress ? "EVM" : "Solana"}\n`);

  const activeDomains = isEvmAddress ? EVM_DOMAINS : SOLANA_DOMAINS;
  const domainIds = Object.values(activeDomains);
  const body = {
    token: "USDC",
    sources: domainIds.map((domain) => ({
      domain,
      depositor: DEPOSITOR_ADDRESS,
    })),
  };

  const res = await fetch(
    "https://gateway-api-testnet.circle.com/v1/balances",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    },
  );

  const result = (await res.json()) as GatewayBalancesResponse;

  let total = 0;
  for (const balance of result.balances) {
    const chain =
      Object.keys(DOMAINS).find(
        (k) => DOMAINS[k as keyof typeof DOMAINS] === balance.domain,
      ) || `Domain ${balance.domain}`;
    const amount = parseFloat(balance.balance);
    console.log(`${chain}: ${amount.toFixed(6)} USDC`);
    total += amount;
  }

  console.log(`\nTotal: ${total.toFixed(6)} USDC`);
}

main().catch((error) => {
  console.error("\nError:", error);
  process.exit(1);
});
You can run it to check whether finality has been reached for recent transactions.
npm run balances

Step 4. Transfer USDC from EVM to EVM

This step transfers USDC from your Arc Testnet Gateway balance to Base Sepolia. Both paths create and submit the burn intent from Arc Testnet first. Direct Mint then retrieves the Gateway attestation and calls gatewayMint(...) on Base Sepolia from your wallet, while the Forwarding Service lets Circle complete the destination mint for you.

4.1. Create the EVM transfer script (Circle Wallets)

You can skip to the full transfer script if you prefer.
touch transfer-from-evm.ts

4.2. Define constants and types

This script uses Arc Testnet as the source chain and Base Sepolia as the destination chain.This direct-mint flow signs the burn intent on Arc Testnet, requests the Gateway attestation, and then calls gatewayMint(...) on Base Sepolia from your wallet.
type GatewayTransferResponse = {
  attestation: string;
  signature: string;
};

const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();

4.3. Add shared chain references

The script uses the shared chain map from config.ts.
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;

const sourceConfig = chainConfig[SOURCE_CHAIN];
const destinationConfig = chainConfig[DEST_CHAIN];

4.4. Create and sign the burn intent

Create an EIP-712 burn intent for the Arc Testnet Gateway balance and sign it with the source Developer-Controlled Wallet.
const burnIntent = createBurnIntent({
  depositorAddress: DEPOSITOR_ADDRESS,
  recipientAddress: RECIPIENT_ADDRESS,
});

const typedData = burnIntentTypedData(burnIntent);

const sigResp = await client.signTypedData({
  walletAddress: DEPOSITOR_ADDRESS,
  blockchain: sourceConfig.walletChain,
  data: stringifyTypedData(typedData),
});

4.5. Request the Gateway attestation

Send the signed burn intent to the Gateway API and validate that the response includes both the attestation and the operator signature needed for minting.
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: stringifyTypedData(requests),
});

4.6. Mint on Base Sepolia

Once the Gateway API returns the attestation set, call gatewayMint(bytes,bytes) on Base Sepolia and wait for Circle Wallet transaction completion.
const tx = await client.createContractExecutionTransaction({
  walletAddress: RECIPIENT_ADDRESS,
  blockchain: destinationConfig.walletChain,
  contractAddress: GATEWAY_MINTER_ADDRESS,
  abiFunctionSignature: "gatewayMint(bytes,bytes)",
  abiParameters: [attestation, operatorSig],
  fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});

await waitForTxCompletion(client, txId, "USDC mint");

4.7. Full EVM direct-mint script (Circle Wallets)

The script builds and signs a burn intent for Arc Testnet, requests the Gateway attestation, and mints on Base Sepolia. Inline comments explain each stage.
transfer-from-evm.ts
import { randomBytes } from "node:crypto";
import {
  chainConfig,
  GATEWAY_API_BASE,
  GATEWAY_WALLET_ADDRESS,
  GATEWAY_MINTER_ADDRESS,
  DEPOSITOR_ADDRESS,
  client,
  waitForTxCompletion,
  addressToBytes32,
  stringifyTypedData,
} from "./config.js";

type GatewayTransferResponse = {
  attestation: string;
  signature: string;
};

const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;

if (!RECIPIENT_ADDRESS) {
  console.error("Missing required env var: RECIPIENT_ADDRESS");
  process.exit(1);
}

const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();

const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;

const sourceConfig = chainConfig[SOURCE_CHAIN];
const destinationConfig = chainConfig[DEST_CHAIN];

const domain = { name: "GatewayWallet", version: "1" };

const EIP712Domain = [
  { name: "name", type: "string" },
  { name: "version", type: "string" },
];

const TransferSpec = [
  { name: "version", type: "uint32" },
  { name: "sourceDomain", type: "uint32" },
  { name: "destinationDomain", type: "uint32" },
  { name: "sourceContract", type: "bytes32" },
  { name: "destinationContract", type: "bytes32" },
  { name: "sourceToken", type: "bytes32" },
  { name: "destinationToken", type: "bytes32" },
  { name: "sourceDepositor", type: "bytes32" },
  { name: "destinationRecipient", type: "bytes32" },
  { name: "sourceSigner", type: "bytes32" },
  { name: "destinationCaller", type: "bytes32" },
  { name: "value", type: "uint256" },
  { name: "salt", type: "bytes32" },
  { name: "hookData", type: "bytes" },
];

const BurnIntent = [
  { name: "maxBlockHeight", type: "uint256" },
  { name: "maxFee", type: "uint256" },
  { name: "spec", type: "TransferSpec" },
];

function createBurnIntent(params: {
  depositorAddress: string;
  recipientAddress?: string;
}) {
  const { depositorAddress, recipientAddress = depositorAddress } = params;

  return {
    maxBlockHeight: MAX_UINT256_DEC,
    maxFee: MAX_FEE,
    spec: {
      version: 1,
      sourceDomain: sourceConfig.domain,
      destinationDomain: destinationConfig.domain,
      sourceContract: GATEWAY_WALLET_ADDRESS,
      destinationContract: GATEWAY_MINTER_ADDRESS,
      sourceToken: sourceConfig.usdc,
      destinationToken: destinationConfig.usdc,
      sourceDepositor: depositorAddress,
      destinationRecipient: recipientAddress,
      sourceSigner: depositorAddress,
      destinationCaller: "0x0000000000000000000000000000000000000000",
      value: TRANSFER_VALUE,
      salt: `0x${randomBytes(32).toString("hex")}`,
      hookData: "0x",
    },
  };
}

function burnIntentTypedData(burnIntent: ReturnType<typeof createBurnIntent>) {
  return {
    types: { EIP712Domain, TransferSpec, BurnIntent },
    domain,
    primaryType: "BurnIntent",
    message: {
      ...burnIntent,
      spec: {
        ...burnIntent.spec,
        sourceContract: addressToBytes32(burnIntent.spec.sourceContract),
        destinationContract: addressToBytes32(
          burnIntent.spec.destinationContract,
        ),
        sourceToken: addressToBytes32(burnIntent.spec.sourceToken),
        destinationToken: addressToBytes32(burnIntent.spec.destinationToken),
        sourceDepositor: addressToBytes32(burnIntent.spec.sourceDepositor),
        destinationRecipient: addressToBytes32(
          burnIntent.spec.destinationRecipient,
        ),
        sourceSigner: addressToBytes32(burnIntent.spec.sourceSigner),
        destinationCaller: addressToBytes32(burnIntent.spec.destinationCaller),
      },
    },
  };
}

async function main() {
  console.log("Transferring 1 USDC from Arc Testnet to Base Sepolia");
  console.log(`Using wallet: ${DEPOSITOR_ADDRESS}`);
  console.log(`Recipient: ${RECIPIENT_ADDRESS}`);

  // [1] Create and sign the Arc Testnet burn intent.
  console.log("\n[1/3] Signing burn intent...");

  const burnIntent = createBurnIntent({
    depositorAddress: DEPOSITOR_ADDRESS,
    recipientAddress: RECIPIENT_ADDRESS,
  });

  const typedData = burnIntentTypedData(burnIntent);

  const sigResp = await client.signTypedData({
    walletAddress: DEPOSITOR_ADDRESS,
    blockchain: sourceConfig.walletChain,
    data: stringifyTypedData(typedData),
  });

  const signature = sigResp.data?.signature;
  if (!signature) throw new Error("Failed to sign burn intent");

  const requests = [{ burnIntent: typedData.message, signature }];

  // [2] Submit the burn intent to the Gateway API.
  console.log("[2/3] Submitting burn intent to Gateway API...");

  const transferResponse = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: stringifyTypedData(requests),
  });

  if (!transferResponse.ok) {
    throw new Error(await transferResponse.text());
  }

  const { attestation, signature: operatorSig } =
    (await transferResponse.json()) as GatewayTransferResponse;

  if (!attestation || !operatorSig) {
    throw new Error("Invalid Gateway API response");
  }

  // [3] Mint on Base Sepolia with the returned attestation set.
  console.log(`[3/3] Minting on ${destinationConfig.chainName}...`);

  const tx = await client.createContractExecutionTransaction({
    walletAddress: RECIPIENT_ADDRESS,
    blockchain: destinationConfig.walletChain,
    contractAddress: GATEWAY_MINTER_ADDRESS,
    abiFunctionSignature: "gatewayMint(bytes,bytes)",
    abiParameters: [attestation, operatorSig],
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });

  const txId = tx.data?.id;
  if (!txId) throw new Error("Failed to submit mint transaction");

  await waitForTxCompletion(client, txId, "USDC mint");

  console.log(
    `Transfer complete. 1 USDC minted on ${destinationConfig.chainName}.`,
  );
  console.log(`Mint transaction ID: ${txId}`);
}

main().catch((error) => {
  console.error("\nError:", error);
  process.exit(1);
});

4.8. Run the EVM direct-mint script

Run the script to burn from your Arc Testnet Gateway balance and call gatewayMint(...) on Base Sepolia.Confirm these values before running:
  • DEPOSITOR_ADDRESS is the source Arc Testnet wallet
  • RECIPIENT_ADDRESS is the destination Base Sepolia wallet
  • the source depositor has a Gateway balance on Arc Testnet
  • the destination wallet can submit the mint transaction on Base Sepolia
Gateway fees are charged per burn intent and are based on the source blockchain you burn from. Choosing where to hold and burn Gateway balances can affect transfer costs. For fee details, see Gateway Fees.
npm run transfer-from-evm
When the transfer succeeds, the script logs the minted amount and mint transaction ID.