Skip to main content
Move USDC from Base Sepolia to a Movement Bardock wallet as USDCx. Circle’s Cross-Chain Transfer Protocol (CCTP) and Forwarding Service move the funds through a dedicated wrapper contract authored by Circle on Arc Testnet and deposit them into xReserve. You don’t deploy a settlement contract or run a relayer for this leg. The transfer has two legs:
  • Base Sepolia to Arc Testnet: CCTP burns the source USDC. On Arc Testnet, the wrapper receives the mint and calls xReserve through DepositForHandler in the same transaction.
  • Arc Testnet to Movement Bardock: xReserve attests the deposit, and the remote blockchain’s settlement process mints USDCx to the recipient.
In this quickstart, Base Sepolia is the CCTP source, Arc Testnet is the CCTP destination, and Movement Bardock is the xReserve remote blockchain. For more on settlement, see How xReserve works. Circle enables this forwarding flow for selected deposit actions, including xReserve deposits. This quickstart follows one testnet route. For other routes, check the CCTP support table, CCTP contract addresses, and xReserve domain reference. You can also use Get domain information to look up xReserve domain IDs. Confirm other routes with Circle before exposing them to users; a domain listing or CCTP support alone doesn’t establish support for the full flow.

Prerequisites

Before you begin, ensure that you’ve:

Step 1: Set up your project

Create a project and install viem:
The last command excludes .env from version control. Open .env in your editor and replace the wallet key and recipient placeholders:
.env
This example uses the public RPC for Base Sepolia. To use your own provider, replace BASE_SEPOLIA_RPC_URL. If you omit it, viem uses the default Base Sepolia RPC. Keep your private key secret. Don’t commit .env or print the key in logs or shell history. You don’t need an Arc wallet for this flow.

Step 2: Configure the testnet route

Keep all parts of the transfer on testnets. Use these values for this example: Create config.ts:
config.ts
The amount is 1 test USDC, excluding the upfront fee. The Movement recipient is the account address, encoded as bytes32, with empty remote hook data.

Step 3: Build the deposit hook data

Build the data for the action that the wrapper runs on Arc. It has four layers: DepositForHandler fills in the amount minted on Arc. Offset 4 is the start of the amount argument after the four-byte function selector. The two hook frames are siblings; don’t nest one in the other. See the CCTP hook format. Create hookData.ts:
hookData.ts
Create request.ts. Both the quote and the burn use the same hook data from this file:
request.ts
Your Base Sepolia account is the source burner, wrapper recovery address, and fee claim’s refundAddress. The recovery address doesn’t change the remote recipient.

Step 4: Request a signed fee quote

Get a price for the transfer before you approve or burn USDC. The quote covers Fast Transfer (PRE_FINALITY) and forwarding (FORWARD), with fees paid in source USDC. The fee is charged in addition to the deposit amount, so the full 1 USDC arrives at xReserve on Arc. See How upfront fees work. Create prepare.ts:
prepare.ts
Generate the request, then call the public v2 quote endpoint:
The response includes signedQuote, feeTotalAmount, feeToken, items, and expiry. The items array breaks down the fee by service. The script uses feeTotalAmount for the total; you don’t need to sum items yourself. The signed quote binds to the amount, destination domain, destination caller, and hook data. If you change the route, amount, or recipient, regenerate request.json and request another quote before submission. Quotes expire soon after issue. The Base Sepolia quote can expire while an approval is pending. The next step checks expiry again after approval; if it expires, repeat the quote request and submission step. Don’t treat the advisory blockEstimatedAt as the authoritative expiry for a block-number quote. See Quote expiry.

Step 5: Approve USDC and submit the burn

Approve the deposit amount plus the quoted fee to TokenMessengerWithFees, then submit the source transaction. Both mintRecipient and destinationCaller must be the wrapper on Arc, encoded as bytes32. Create submit.ts. It checks the source network and balance, waits for a successful approval, checks quote expiry, and simulates the burn before sending it:
submit.ts
Before submitting, record the recipient’s USDCx balance using the query in Step 6.3. You can compare it after settlement. Run the script once to submit the deposit:
Save the source burn hash. A successful source receipt means USDC was burned. USDCx may still be pending. If the burn status is unknown, check its receipt before sending anything else. If the burn succeeded, track it. Don’t run the script again and start another transfer. This example pays the fee in USDC and sends no native value with the burn. To pay in the source blockchain’s native token instead, request a quote with the zero address as feeToken, approve only the deposit amount, and attach the quoted native fee as value. Don’t mix a native-token quote with the USDC fee handling in this script. See Transfer USDC with upfront fees.

Step 6: Track both settlement legs

Show progress for each stage: source burn, Arc forward, xReserve attestation, and remote mint. Only the last stage confirms that the recipient has USDCx.

6.1. Find the Arc forwarding transaction

Use your Base Sepolia burn hash with the CCTP message API:
Replace YOUR_SOURCE_BURN_TX_HASH with the source burn hash printed by submit.ts in Step 5. The forwarding hash is inside each entry in messages. For example, an abbreviated response after forwarding looks like this (hashes are placeholders):
Poll until the matching message has a forwardTxHash. If it’s pending, keep tracking the same transfer. Check the Arc receipt for success and the xReserve deposit event before moving to the next leg. If a source transaction contains multiple CCTP messages, match the message to your amount, destination caller, and hook data rather than selecting the first entry.

6.2. Fetch the xReserve attestation

Use the successful Arc transaction hash, rather than the Base Sepolia hash, with the xReserve attestation API:
Replace YOUR_ARC_FORWARD_TX_HASH with the forwardTxHash from the CCTP message API response. Check that the Arc receipt succeeded before using that hash here. An abbreviated response for this example looks like this (hex values are placeholders):
The response’s attestations array has the payload, message hash, signature, and remote domain. Match it to your deposit and remote domain. This proves that xReserve attested the deposit. It doesn’t prove that the remote blockchain minted USDCx.

6.3. Confirm the remote mint

Query the recipient’s USDCx balance on Movement Bardock using its public RPC:
Replace YOUR_MOVEMENT_RECIPIENT with the address you set in .env. The response is an array containing a balance string in base units. For example, ["1000000"] means 1 USDCx. For this deposit, expect the balance to increase by 1,000,000 base units from the value recorded before the burn, assuming no other activity on the recipient account. Use the Movement testnet explorer to confirm a successful mint transaction for the intended recipient and amount. Match its deposit nonce to the xReserve attestation so that you don’t mistake an unrelated credit for this deposit. See the Movement deposit quickstart for more on the remote integration. Keep the source hash, Arc hash, xReserve message hash, and remote mint reference together for support and reconciliation.

Step 7: Handle failures and delayed deposits

Check your route before burning. The quote service checks that the forwarding workflow is enabled. It doesn’t check every xReserve argument in the hook data. A quote doesn’t prove that a remote domain, recipient, or remote hook data is valid. Test each route and recipient format before users can use it. A paused or reverting destination action can require coordinated manual replay of the unconsumed CCTP message after the cause is resolved. This retries the original deposit; it is separate from the planned recovery feature. Don’t assume the service automatically retries every failed deposit. A source burn can’t be undone by requesting another quote. Upfront fees aren’t refunded. The fee claim’s refundAddress is reserved for potential future use; setting it doesn’t promise a refund if a later leg fails. The wrapper’s recoveryAddress is a separate field reserved for compatibility with a future recovery feature. The wrapper does not implement a recovery function. Supplying a recoveryAddress does not enable fund recovery.