Skip to main content
This example transfers 10 USDC from Base Sepolia to Arc Testnet, paying both the Fast Transfer and Forwarding Service fees upfront so the recipient receives the full transfer amount on the destination blockchain. You can use the same steps for any EVM source blockchain where both upfront fees and Fast Transfer are available, and any destination blockchain that the Forwarding Service supports.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+
  • Created a TypeScript project and installed the viem package
  • Created a wallet with the private key accessible from your local development environment
  • Funded the wallet on Base Sepolia with:
  • Created a .env file with your private key
Keep your private key secret. Store it in a .env file that’s listed in .gitignore, and never commit it or expose it in logs or shell history.

Steps

Step 1: Request a signed quote from the Quote API

Request a quote from the Quote API for the fees you want to pay upfront. This example requests both FORWARD (Forwarding Service) and PRE_FINALITY (Fast Transfer) fees, and sets feeToken to the source blockchain’s USDC address to pay the fee in USDC. The following request uses source domain 6 (Base Sepolia) and destination domain 26 (Arc Testnet). The USDC address and decimals come from the token definition in viem/tokens, so you don’t hardcode either one:
Example response:
The signedQuote is the blob you submit onchain (truncated here for brevity). The feeTotalAmount is the total fee in feeToken minor units, and items breaks it down per fee type. This quote uses BLOCK_NUMBER expiry, so it stays valid until expiresAtBlock on the source blockchain.
Quotes expire after a short, per-blockchain window (see Quote expiry). Request the quote immediately before you submit the transfer, and submit before it expires.

Step 2: Calculate the approval amount

To pay the fee in USDC, approve the TokenMessengerWithFees contract to spend both the transfer amount and the quoted fee. The contract pulls the transfer amount for the burn and the fee for collection.
Because you pay the fee upfront, the recipient receives the full transferAmount on Arc Testnet. The fee is charged separately from the amount minted.

Step 3: Approve the USDC transfer

Grant approval for the TokenMessengerWithFees contract on Base Sepolia to spend USDC from your wallet. Approve at least the approvalAmount calculated in Step 2. Registering the usdc token on the wallet client gives you the client.token.approve action, so you don’t have to supply the ERC-20 ABI yourself:
sendTransaction resolves once the node accepts the transaction, not once it’s mined. Without the wait, a burn submitted immediately after the approval can revert because the allowance isn’t set yet.

Step 4: Sign and broadcast a depositForBurnWithFees transaction

Call depositForBurnWithFees on the TokenMessengerWithFees contract, passing a QuoteClaim that bundles the signedQuote with your refundAddress. The contract infers Fast Transfer from the PRE_FINALITY fee and adds the default cctp-forward hook because the quote includes a FORWARD fee, so you don’t pass maxFee, minFinalityThreshold, or hook data. This example uses the QuoteClaim overload, which takes the opaque signedQuote bytes directly. The contract also exposes a DecodedQuoteClaim overload for advanced cases where you decode the quote in your own code; see the full ABI in Contract interfaces.
Use transferAmount (not the amount plus fee) for the amount parameter. The fee is collected separately, and the recipient receives the full transferAmount.
To pay the fee in the source blockchain’s native gas token instead of USDC, omit feeToken from the quote request in Step 1. Then approve only the transferAmount in Step 3, and attach the quoted fee as the transaction’s native value in Step 4:
Because the quote binds to your call arguments, a mismatch reverts onchain and costs you gas. Request the quote with the same amount, destination domain, and burn token you submit onchain so the values match. Once the burn transaction is confirmed on Base Sepolia, Circle attests the burn and the Forwarding Service mints USDC on Arc Testnet. Because the fee is paid upfront, the recipient receives the full transferAmount.

Step 5: Verify the mint transaction

After the burn 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 blockchain. The attestation may take time to become available, so poll the API until the message is ready:

Full example code

The following is a complete example of how to transfer USDC from Base Sepolia to Arc Testnet with upfront fees. Remember to set the PRIVATE_KEY environment variable.
script.ts
Run the script:

Pay fees upfront with a custom hook

The preceding steps use depositForBurnWithFees, which builds the default cctp-forward hook for you. To attach your own hook data on the destination blockchain, alongside forwarding, use depositForBurnWithHookAndFees and provide the hook data explicitly.
The quote binds to your call parameters, including the hook data. Request the quote with the same hook data you submit onchain. A quote requested with custom hook data is rejected by depositForBurnWithFees (which applies the default hook), and a quote requested without custom hook data is rejected by depositForBurnWithHookAndFees. Always pair the entry point with a matching quote.

Build the hook data

Hook data is a sequence of hooks, each with a fixed 32-byte header followed by its payload: The first hook must be the cctp-forward hook so that forwarding runs. You append your own hook as another complete, correctly framed entry: appending raw, unframed bytes causes the destination to reject the hook data. For the cctp-forward layout, see Forwarding Service hook format.

Request a matching quote

Include the same hookData in the FORWARD request’s params so the quote binds to it:

Submit the transfer with depositForBurnWithHookAndFees

Pass the same hookData as a parameter, along with the QuoteClaim:
Approve USDC and verify the mint the same way as Step 3 and Step 5.