Prerequisites
Before you begin, ensure that you’ve:- Installed Node.js v22+
- Created a TypeScript project and installed the
viempackage - Created a wallet with the private key accessible from your local development environment
- Funded the wallet on Base Sepolia with:
- Base Sepolia ETH for gas from a public faucet
- Base Sepolia USDC from the Circle Faucet
- Created a
.envfile with your private key
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 bothFORWARD (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:
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.
Step 2: Calculate the approval amount
To pay the fee in USDC, approve theTokenMessengerWithFees 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.
transferAmount on Arc Testnet. The fee is charged separately from the amount
minted.
Step 3: Approve the USDC transfer
Grant approval for theTokenMessengerWithFees
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.Pay the fee in the native gas token
Pay the fee in the native gas token
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: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 theforwardTxHash, 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 thePRIVATE_KEY environment
variable.
script.ts
Pay fees upfront with a custom hook
The preceding steps usedepositForBurnWithFees, 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.
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 samehookData 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: