> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# Quickstart: Deposit into xReserve from CCTP blockchains

> Deposit USDC from Base Sepolia through Arc Testnet into xReserve to receive USDCx on Movement Bardock.

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](https://developers.circle.com/xreserve/concepts/how-xreserve-works.md).

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](https://developers.circle.com/cctp/concepts/supported-chains-and-domains.md#supported-blockchains),
[CCTP contract addresses](https://developers.circle.com/cctp/references/contract-addresses.md), and
[xReserve domain reference](https://developers.circle.com/xreserve/references/supported-blockchains-and-domains.md).
You can also use [Get domain information](https://developers.circle.com/api-reference/xreserve/all/get-info.md)
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:

* Installed [Node.js v22.18+](https://nodejs.org/) and `curl`.
* Created a Base Sepolia wallet and funded it with test USDC from the
  [Circle Faucet](https://faucet.circle.com/) and Base Sepolia ETH for gas.
* Created a Movement Bardock recipient account. See the
  [Movement deposit quickstart](https://developers.circle.com/xreserve/tutorials/deposit-usdc-into-xreserve.md#movement).

## Step 1: Set up your project

Create a project and install `viem`:

```bash theme={null}
mkdir xreserve-cctp-deposit
cd xreserve-cctp-deposit
npm init -y
npm pkg set type=module
npm install viem
printf '%s\n' '.env' >> .gitignore
```

The last command excludes `.env` from version control. Open `.env` in your
editor and replace the wallet key and recipient placeholders:

```text .env theme={null}
PRIVATE_KEY=your-base-sepolia-private-key
MOVEMENT_RECIPIENT=your-movement-bardock-account-address
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
```

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:

| Setting | Value |
| - | - |
| Base Sepolia CCTP domain | `6` |
| Arc Testnet CCTP and xReserve domain | `26` |
| Movement Bardock xReserve remote domain | `10005` |
| Base Sepolia USDC | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Base Sepolia TokenMessengerWithFees | `0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A` |
| Arc Testnet Circle wrapper | `0xEdC81040756AcCfF070c21D37b265b9D0b5Ba45e` |
| Arc Testnet DepositForHandler | `0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48` |
| Arc Testnet xReserve | `0x008888878f94C0d87defdf0B07f46B93C1934442` |
| Arc Testnet USDC | `0x3600000000000000000000000000000000000000` |
| CCTP API | `https://iris-api-sandbox.circle.com` |
| xReserve API | `https://xreserve-api-testnet.circle.com` |

Create `config.ts`:

```typescript config.ts expandable theme={null}
import {
  createPublicClient,
  createWalletClient,
  http,
  padHex,
  parseUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";

const privateKey = process.env.PRIVATE_KEY;
const recipient = process.env.MOVEMENT_RECIPIENT;
if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
  throw new Error("Set PRIVATE_KEY to your Base Sepolia wallet key.");
}
if (!recipient || !/^0x[0-9a-fA-F]{1,64}$/.test(recipient)) {
  throw new Error(
    "Set MOVEMENT_RECIPIENT to your Movement Bardock account address.",
  );
}

export const account = privateKeyToAccount(privateKey as `0x${string}`);
export const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: http(process.env.BASE_SEPOLIA_RPC_URL),
});
export const walletClient = createWalletClient({
  account,
  chain: baseSepolia,
  transport: http(process.env.BASE_SEPOLIA_RPC_URL),
});

export const SOURCE_DOMAIN = 6;
export const ARC_DOMAIN = 26;
export const REMOTE_DOMAIN = 10005;
export const AMOUNT = parseUnits("1", 6);
export const SOURCE_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
export const TMWF = "0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A";
export const CIRCLE_WRAPPER = "0xEdC81040756AcCfF070c21D37b265b9D0b5Ba45e";
export const DEPOSIT_HANDLER = "0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48";
export const XRESERVE = "0x008888878f94C0d87defdf0B07f46B93C1934442";
export const ARC_USDC = "0x3600000000000000000000000000000000000000";

// Movement uses its account address, left-padded to bytes32, and empty hook data.
export const REMOTE_RECIPIENT = padHex(
  `0x${recipient.slice(2).padStart(64, "0")}`,
  { size: 32 },
);
export const XRESERVE_HOOK_DATA = "0x";
```

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:

| Layer | Contents |
| - | - |
| xReserve call | `depositToRemote` with a zero amount placeholder and your remote recipient |
| Handler calldata | xReserve as both `depositContract` and `approvalTarget`, the deposit calldata, and `amountIndices = [4]` |
| Wrapper payload | Version `1`, the source burner as `recoveryAddress`, DepositForHandler, and its calldata |
| Composable hook data | A `cctp-forward` frame first, followed by a sibling frame containing the wrapper payload |

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](https://developers.circle.com/cctp/concepts/forwarding-service.md#hook-format).

Create `hookData.ts`:

```typescript hookData.ts expandable theme={null}
import {
  type Address,
  type Hex,
  concatHex,
  encodeAbiParameters,
  encodeFunctionData,
  numberToHex,
  padHex,
  parseAbi,
  parseAbiParameters,
  size,
  stringToHex,
} from "viem";

interface DepositHookParams {
  recoveryAddress: Address;
  handler: Address;
  xreserve: Address;
  localToken: Address;
  remoteDomain: number;
  remoteRecipient: Hex;
  maxFee: bigint;
  xReserveHookData: Hex;
}

const WRAPPER_HOOK_MAGIC: Hex =
  "0x636972636c652d67656e657269632d6578656375746f7200";

function encodeHookFrame(magic: Hex, payload: Hex): Hex {
  return concatHex([
    magic,
    numberToHex(1, { size: 4 }),
    numberToHex(size(payload), { size: 4 }),
    payload,
  ]);
}

export function buildDepositHookData(params: DepositHookParams): Hex {
  const depositCalldata = encodeFunctionData({
    abi: parseAbi([
      "function depositToRemote(uint256 value, uint32 remoteDomain, bytes32 remoteRecipient, address localToken, uint256 maxFee, bytes hookData)",
    ]),
    functionName: "depositToRemote",
    args: [
      0n,
      params.remoteDomain,
      params.remoteRecipient,
      params.localToken,
      params.maxFee,
      params.xReserveHookData,
    ],
  });
  const handlerCalldata = encodeAbiParameters(
    parseAbiParameters(
      "address depositContract, address approvalTarget, bytes depositCalldata, uint256[] amountIndices",
    ),
    [params.xreserve, params.xreserve, depositCalldata, [4n]],
  );
  const wrapperPayload = encodeAbiParameters(
    parseAbiParameters(
      "uint8 payloadVersion, bytes32 recoveryAddress, address handler, bytes handlerCalldata",
    ),
    [
      1,
      padHex(params.recoveryAddress, { size: 32 }),
      params.handler,
      handlerCalldata,
    ],
  );
  return concatHex([
    encodeHookFrame(stringToHex("cctp-forward", { size: 24 }), "0x"),
    encodeHookFrame(WRAPPER_HOOK_MAGIC, wrapperPayload),
  ]);
}
```

Create `request.ts`. Both the quote and the burn use the same hook data from
this file:

```typescript request.ts expandable theme={null}
import { padHex } from "viem";
import {
  account,
  AMOUNT,
  ARC_USDC,
  DEPOSIT_HANDLER,
  CIRCLE_WRAPPER,
  REMOTE_DOMAIN,
  REMOTE_RECIPIENT,
  SOURCE_USDC,
  XRESERVE,
  XRESERVE_HOOK_DATA,
} from "./config.ts";
import { buildDepositHookData } from "./hookData.ts";

export const wrapperBytes32 = padHex(CIRCLE_WRAPPER, { size: 32 });
export const hookData = buildDepositHookData({
  recoveryAddress: account.address,
  handler: DEPOSIT_HANDLER,
  xreserve: XRESERVE,
  localToken: ARC_USDC,
  remoteDomain: REMOTE_DOMAIN,
  remoteRecipient: REMOTE_RECIPIENT,
  maxFee: 0n,
  xReserveHookData: XRESERVE_HOOK_DATA,
});
export const quoteRequest = {
  amount: AMOUNT.toString(),
  feeToken: SOURCE_USDC,
  requests: [
    { type: "PRE_FINALITY" },
    {
      type: "FORWARD",
      params: { destinationCaller: wrapperBytes32, hookData },
    },
  ],
};
```

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](https://developers.circle.com/cctp/concepts/how-upfront-fees-work.md).

Create `prepare.ts`:

```typescript prepare.ts theme={null}
import { writeFileSync } from "node:fs";
import { quoteRequest } from "./request.ts";

writeFileSync("request.json", JSON.stringify(quoteRequest, null, 2));
console.log(
  "Created request.json with the amount, fee token, and composable hook data.",
);
```

Generate the request, then call the public v2 quote endpoint:

```bash theme={null}
node --env-file=.env prepare.ts
curl --fail-with-body --silent --show-error \
  -X POST "https://iris-api-sandbox.circle.com/v2/quote/burn/usdc/6/26" \
  -H "Content-Type: application/json" \
  --data-binary @request.json \
  -o quote.json
```

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](https://developers.circle.com/cctp/concepts/upfront-fee-quotes.md#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:

```typescript submit.ts expandable theme={null}
import { readFileSync } from "node:fs";
import { type Hex, erc20Abi, parseAbi } from "viem";
import { baseSepolia } from "viem/chains";
import {
  account,
  AMOUNT,
  ARC_DOMAIN,
  publicClient,
  SOURCE_USDC,
  TMWF,
  walletClient,
} from "./config.ts";
import { wrapperBytes32, hookData, quoteRequest } from "./request.ts";

type Quote = {
  signedQuote: Hex;
  feeTotalAmount: string;
  feeToken: string;
  expiry:
    | { mode: "TIMESTAMP"; expiresAt: number }
    | { mode: "BLOCK_NUMBER"; expiresAtBlock: number };
};

if (
  JSON.stringify(JSON.parse(readFileSync("request.json", "utf8"))) !==
  JSON.stringify(quoteRequest)
) {
  throw new Error("Configuration changed. Repeat Step 4 before submitting.");
}
const quote: Quote = JSON.parse(readFileSync("quote.json", "utf8"));
if (
  !/^0x(?:[0-9a-fA-F]{2})+$/.test(quote.signedQuote) ||
  !/^\d+$/.test(quote.feeTotalAmount) ||
  quote.feeToken?.toLowerCase() !== SOURCE_USDC.toLowerCase()
) {
  throw new Error("Expected a successful quote with fees in source USDC.");
}

async function checkQuoteExpiry(): Promise<void> {
  const expiry = quote.expiry;
  const block = await publicClient.getBlock();
  if (
    expiry?.mode === "BLOCK_NUMBER" &&
    Number.isSafeInteger(expiry.expiresAtBlock)
  ) {
    if (block.number < BigInt(expiry.expiresAtBlock)) return;
  } else if (
    expiry?.mode === "TIMESTAMP" &&
    Number.isSafeInteger(expiry.expiresAt)
  ) {
    if (
      BigInt(Math.floor(Date.now() / 1000)) < BigInt(expiry.expiresAt) &&
      block.timestamp < BigInt(expiry.expiresAt)
    )
      return;
  }
  throw new Error(
    "Quote expired or expiry is invalid. Request a new quote in Step 4.",
  );
}

if ((await publicClient.getChainId()) !== baseSepolia.id) {
  throw new Error("The source RPC must point to Base Sepolia.");
}
await checkQuoteExpiry();
const approvalAmount: bigint = AMOUNT + BigInt(quote.feeTotalAmount);
const balance: bigint = await publicClient.readContract({
  address: SOURCE_USDC,
  abi: erc20Abi,
  functionName: "balanceOf",
  args: [account.address],
});
if (balance < approvalAmount)
  throw new Error("Insufficient USDC for the deposit and fee.");
const allowance: bigint = await publicClient.readContract({
  address: SOURCE_USDC,
  abi: erc20Abi,
  functionName: "allowance",
  args: [account.address, TMWF],
});
if (allowance < approvalAmount) {
  const approvalHash = await walletClient.writeContract({
    address: SOURCE_USDC,
    abi: erc20Abi,
    functionName: "approve",
    args: [TMWF, approvalAmount],
  });
  console.log("Approval transaction:", approvalHash);
  const approval = await publicClient.waitForTransactionReceipt({
    hash: approvalHash,
  });
  if (approval.status !== "success") throw new Error("USDC approval reverted.");
}

await checkQuoteExpiry();
const { request } = await publicClient.simulateContract({
  account,
  address: TMWF,
  abi: parseAbi([
    "function depositForBurnWithHookAndFees(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, bytes hookData, (bytes signedQuote, address refundAddress) claim) payable",
  ]),
  functionName: "depositForBurnWithHookAndFees",
  args: [
    AMOUNT,
    ARC_DOMAIN,
    wrapperBytes32,
    SOURCE_USDC,
    wrapperBytes32,
    hookData,
    {
      signedQuote: quote.signedQuote,
      refundAddress: account.address,
    },
  ],
  value: 0n,
});
const burnHash = await walletClient.writeContract(request);
console.log("Source burn transaction:", burnHash);
const burn = await publicClient.waitForTransactionReceipt({ hash: burnHash });
if (burn.status !== "success")
  throw new Error("Source burn reverted. No transfer started.");
console.log(
  "Source burn confirmed. Track this transaction through both settlement legs.",
);
```

Before submitting, record the recipient's USDCx balance using the query in
[Step 6.3](#63-confirm-the-remote-mint). You can compare it after settlement.

Run the script once to submit the deposit:

```bash theme={null}
node --env-file=.env submit.ts
```

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](https://developers.circle.com/cctp/howtos/transfer-usdc-with-upfront-fees.md).

## 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](https://developers.circle.com/api-reference/cctp/all/get-messages-v2.md):

```bash theme={null}
curl --fail-with-body --silent --show-error \
  "https://iris-api-sandbox.circle.com/v2/messages/6?transactionHash=YOUR_SOURCE_BURN_TX_HASH"
```

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):

```json theme={null}
{
  "messages": [
    {
      "forwardTxHash": "0x..."
    }
  ],
  "sourceTxHash": "0x..."
}
```

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](https://developers.circle.com/api-reference/xreserve/all/get-attestations-by-tx-hash.md):

```bash theme={null}
curl --fail-with-body --silent --show-error \
  "https://xreserve-api-testnet.circle.com/v1/attestations?txHash=YOUR_ARC_FORWARD_TX_HASH"
```

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):

```json theme={null}
{
  "attestations": [
    {
      "payload": "0x...",
      "messageHash": "0x...",
      "attestation": "0x...",
      "remoteDomain": 10005
    }
  ]
}
```

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](https://docs.movementnetwork.xyz/devs/networkEndpoints#movement-testnet):

```bash theme={null}
curl --fail-with-body --silent --show-error \
  -X POST "https://testnet.movementnetwork.xyz/v1/view" \
  -H "Content-Type: application/json" \
  --data '{
    "function": "0x9d7662ccebd1a6362a5eaf06cd3a88bb2e11ce8cd4209b196a82b69339774035::usdcx::balance_of",
    "type_arguments": [],
    "arguments": ["YOUR_MOVEMENT_RECIPIENT"]
  }'
```

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](https://explorer.movementnetwork.xyz/?network=bardock+testnet)
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](https://developers.circle.com/xreserve/tutorials/deposit-usdc-into-xreserve.md#movement)
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.

| Situation | What to do |
| - | - |
| Quote is rejected or expired | Check route support and request parameters, then request a new quote. Don't submit a burn with an expired quote. |
| Source burn reverts | Check its receipt. A reverted source transaction doesn't start a CCTP transfer. Fix the cause before submitting again. |
| Arc execution reverts because deposits are paused | The CCTP mint and xReserve deposit revert together, leaving the CCTP message unconsumed. Contact Circle support with the source and Arc transaction references. Support can coordinate replay after the pause ends. |
| Remote domain or hook data is invalid, or a blocklist prevents execution | Stop and contact Circle support. A CCTP message can be unused even if its original action can never succeed. Don't promise automatic recovery or an immediate return of funds. |
| Arc deposit succeeds but the remote mint is pending | Track the xReserve attestation and remote settlement process. Don't repeat the source burn. |

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](https://developers.circle.com/cctp/references/contract-interfaces.md#input-types).
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.
