# Agent Stack Source: https://developers.circle.com/agent-stack Empower your AI agents to autonomously operate wallets, transact onchain, and pay for API services through Circle's agent-native tooling. Circle Agent Stack lets your AI agent hold and transact USDC and other tokens across blockchains, discover and pay for x402 services, and operate within built-in compliance guardrails. Use with [Claude Code](https://claude.com/claude-code), [Cursor](https://cursor.com/), [Codex](https://openai.com/codex), [OpenClaw](https://openclaw.ai/), or any custom AI agent. ## What you can do * **Build with an agent-native interface**: Use [Circle CLI](/agent-stack/circle-cli) and [Circle Skills](/ai/skills) to give your agent access to [Circle Wallets](/wallets), [CCTP](/cctp), and [Gateway](/gateway) from a single command interface. * **Give your AI agent wallets**: Use [Agent Wallets](/agent-stack/agent-wallets) to hold and spend USDC and other tokens with customizable spending controls and built-in compliance guardrails. * **Discover and pay for services on demand**: Search [x402-compatible APIs](https://agents.circle.com/services) and pay per request, without subscriptions or API keys. * **Operate onchain across blockchains**: Trade tokens, bridge USDC, and execute onchain strategies across [supported blockchains](/agent-stack/agent-wallets/supported-blockchains). ## The agent stack A command-line tool for managing agent wallets, installing skills, and accessing the Circle product suite from any agent framework. Wallets for AI agents with custom spending policies, multichain support, and built-in compliance controls. Gasless across blockchains. Gasless USDC payments at sub-cent scale. Pay for x402-compatible services from your AI agent. Powered by Gateway. A curated, compliance-first service catalog where AI agents can discover and pay for USDC-priced services. Open-source skills that give your AI agent specialized knowledge for building with Circle products. ## Dive deeper * Get started with the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart). * Make your first nanopayment with the [Agent Nanopayments quickstart](/agent-stack/agent-nanopayments/quickstart). * Browse [Circle CLI commands](/agent-stack/circle-cli/command-reference). # Agent Nanopayments Source: https://developers.circle.com/agent-stack/agent-nanopayments Gas-free, batched USDC payments at sub-cent scale for AI agents. Agent Nanopayments, built on [Gateway Nanopayments](/gateway/nanopayments), let your AI agent pay for [x402](/gateway/nanopayments/concepts/x402)-compatible services in sub-cent USDC. These payments would normally be uneconomical due to per-payment gas costs, but batching them into a single onchain settlement makes high-frequency machine-to-machine commerce viable. Your agent uses [Circle CLI](/agent-stack/circle-cli) to deposit USDC into a Gateway balance, discover services, and pay for them. ## Get started Follow the [quickstart](/agent-stack/agent-nanopayments/quickstart) to deposit USDC, find a service, and make a nanopayment. ## Use cases Pay for x402-compatible APIs on a per-request basis. No subscriptions or per-service sign-ups. Pay for compute, data, or storage at usage scale. Sub-cent payments make granular billing viable. Enable machine-to-machine payments at high frequency, with batched settlement keeping costs predictable. Find x402-compatible services on [Circle Agent Marketplace](https://agents.circle.com/services) and pay using your [agent wallet](/agent-stack/agent-wallets). ## Features Gateway batches many payment authorizations and settles them onchain in a single transaction, amortizing gas across thousands of payments. Your agent pays no per-transaction gas. Learn more about [batched settlement](/gateway/nanopayments/concepts/batched-settlement). Nanopayments use the [x402 standard](/gateway/nanopayments/concepts/x402), an open HTTP-native payment protocol built around the `402 Payment Required` status code. Sellers declare payment requirements, your agent signs a payment payload, and the exchange happens in a single request-response cycle. Your nanopayments balance can live on any [Gateway-supported blockchain](/gateway/references/supported-blockchains). To integrate x402 and Gateway Nanopayments in your application code instead of Circle CLI, see the [Nanopayments buyer quickstart](/gateway/nanopayments/quickstarts/buyer). # Nanopayment Operations Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations Tasks your agent can perform using Circle CLI to deposit USDC, pay for services, and withdraw funds. Nanopayment operations are tasks your agent can perform using [Circle CLI](/agent-stack/circle-cli). Full command syntax is in the [CLI Command Reference](/agent-stack/circle-cli/command-reference). The following table describes common nanopayment operations and the CLI commands to perform them. | Operation | What it does | Command | | :------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------- | :------------------------ | | [Deposit for nanopayments](/agent-stack/agent-nanopayments/operations/deposit) | Add USDC to your Gateway balance to fund nanopayments. | `circle gateway deposit` | | [Pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service) | Discover and pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API services using USDC. | `circle services pay` | | [Withdraw your balance](/agent-stack/agent-nanopayments/operations/withdraw) | Move remaining USDC from your Gateway balance back to your agent wallet on the same blockchain. | `circle gateway withdraw` | # How to: Deposit for Nanopayments Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/deposit Deposit USDC into Gateway to enable gas-free, sub-cent payments powered by Circle Gateway. Deposit USDC into Gateway once, then make payments without incurring gas costs on each transaction. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Steps Deposit USDC with your amount, wallet address, and blockchain. Direct deposits work from any [Gateway-supported blockchain](/gateway/references/supported-blockchains): ```bash theme={null} circle gateway deposit --amount 5 --address 0xYourWalletAddress --chain BASE --method direct ``` Use `--method eco` for fast deposits through Eco, which supports Base as the source blockchain and settles balances on Polygon PoS. Eco is a third-party fast-deposit service that Circle does not operate or audit. Review [Eco's docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits) and test the flow before using it in production. Confirm the deposit arrived: ```bash theme={null} circle gateway balance --address 0xYourWalletAddress --chain BASE ``` The command returns your current Gateway balance. A non-zero value confirms the deposit arrived. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options. # How to: Pay for a Service Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/pay-for-service Discover and pay for x402-compatible API services using USDC. Pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API services directly from your agent wallet. Payment is processed before the request is forwarded to the service. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). Most x402 services require a Gateway balance. See [Deposit for nanopayments](/agent-stack/agent-nanopayments/operations/deposit) to set one up. ## Steps Browse available services at [agents.circle.com/services](https://agents.circle.com/services), or search by keyword from the CLI: ```bash theme={null} circle services search "weather data" ``` To inspect the payment requirements for a specific URL before paying: ```bash theme={null} circle services inspect https://api.example.com/weather ``` Run `circle services pay` with the service URL and your wallet details. Use `--max-amount` to set a spending cap and avoid unexpected charges. Use `--estimate` to preview payment requirements without paying: ```bash theme={null} # Preview payment requirements without paying circle services pay https://api.example.com/weather \ --address 0xYourWalletAddress \ --chain BASE \ --estimate # Pay for the service circle services pay https://api.example.com/weather \ --address 0xYourWalletAddress \ --chain BASE \ --max-amount 0.01 ``` The CLI prints the service's response body. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including `-X` for HTTP method and `-d` for a request body. # How to: Withdraw Your Balance Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/withdraw Withdraw remaining USDC from your Gateway balance back to your agent wallet. Withdraw remaining USDC from your Gateway balance back to your agent wallet on the same blockchain. Useful when you're done making nanopayments and want to recover unused funds. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). USDC is minted on the same blockchain as your Gateway balance. Crosschain withdrawals aren't supported. ## Steps Run `circle gateway withdraw` with the amount, your wallet address, and the blockchain where your Gateway balance lives: ```bash theme={null} circle gateway withdraw --amount 1 --address 0xYourWalletAddress --chain BASE ``` To send the withdrawn USDC to a different address, add `--recipient`: ```bash theme={null} circle gateway withdraw --amount 1 --address 0xYourWalletAddress --chain BASE --recipient 0xOtherAddress ``` Not sure which blockchain your balance lives on? Run `circle gateway balance --all` to see all blockchains. Check that your Gateway balance decreased: ```bash theme={null} circle gateway balance --address 0xYourWalletAddress --chain BASE ``` Then check that your wallet balance increased: ```bash theme={null} circle wallet balance --address 0xYourWalletAddress --chain BASE ``` See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options. # Quickstart: Make a Nanopayment Source: https://developers.circle.com/agent-stack/agent-nanopayments/quickstart Deposit USDC into Gateway, find an x402-compatible service, and pay for it with your agent wallet. By the end of this quickstart, your agent will have made a nanopayment to an x402-compatible service. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Steps Deposit 5 USDC from your wallet on Base: ```bash theme={null} circle gateway deposit --amount 5 --address 0xYourWalletAddress --chain BASE --method direct ``` Search the Circle Agent Marketplace for available services: ```bash theme={null} circle services search "weather" ``` Pick a service and inspect its payment requirements: ```bash theme={null} circle services inspect https://api.example.com/weather ``` Pay with your Gateway balance on Base. Replace `0xYourWalletAddress` with your wallet address: ```bash theme={null} circle services pay https://api.example.com/weather \ --address 0xYourWalletAddress \ --chain BASE \ --max-amount 0.01 ``` The CLI prints the service's response body. The payment settles against your Gateway balance. Confirm your remaining Gateway balance: ```bash theme={null} circle gateway balance --address 0xYourWalletAddress --chain BASE ``` ## Next steps * [Pay for more services](/agent-stack/agent-nanopayments/operations/pay-for-service) using your remaining balance. * [Withdraw your balance](/agent-stack/agent-nanopayments/operations/withdraw) back to your wallet when you're done. # Agent Wallets Source: https://developers.circle.com/agent-stack/agent-wallets Wallets that let your agent autonomously hold, spend, trade, and earn USDC and other tokens with built-in spending controls. Agent Wallets let your AI agent hold funds and transact onchain autonomously, within spending policies you define. Your agent operates the wallet through Circle CLI without writing integration code. Built on [Circle's user-controlled wallets](/wallets/user-controlled) with 2-of-2 MPC key management, key shares are never exposed to the agent. The user retains custody, and Circle cannot unilaterally move funds without their involvement. All transfers are screened against sanctions controls before submission onchain. ## Get started Paste this prompt into your AI agent: ```text theme={null} curl -sL https://agents.circle.com/skills/setup.md and follow the instructions to set up Circle Agent Wallet ``` The agent installs Circle CLI, creates and funds your agent wallet, and helps you discover and pay for services with it. Prefer a manual setup? Follow the [quickstart](/agent-stack/agent-wallets/quickstart) instead. ## Use cases Run onchain strategies like dollar-cost averaging or token monitoring for autonomous execution within user-defined rules. Execute real-world tasks like booking flights or paying for subscriptions within a scoped USDC budget. Hold, transfer, bridge, and swap USDC across all [supported blockchains](/agent-stack/agent-wallets/supported-blockchains) from a single agent wallet. Set [transfer limits, recipient allowlists, and contract blocklists](/agent-stack/agent-wallets/wallet-operations/custom-policies) per agent wallet. Your agent operates only within rules you define. When running autonomous trading strategies, start with small amounts and validate your approach before scaling. Only commit funds you are comfortable spending. ## Features Built on [Circle user-controlled wallets](/wallets/user-controlled). Key shares are never exposed to the agent. Users retain custody while agents operate in defined spending limits. Operate wallets through Circle CLI commands from any agent framework. No custom integration code required. Set USDC spending limits for outbound transfers and x402 payments. Limits can be time-bound (for example, daily, or monthly). Configure allowlists and blocklists for wallet and contract addresses. Pair your agent wallet with [Agent Nanopayments](/agent-stack/agent-nanopayments) for gasless, sub-cent USDC payments to [x402](/gateway/nanopayments/concepts/x402)-compatible services. All transfers are screened against sanctions controls before submission onchain. Transactions involving sanctioned entities are blocked, ensuring agents operate within regulatory requirements. Agent Wallets support USDC, EURC, and other ERC20 tokens, and native tokens (for example, ETH, MATIC). USDC is the primary asset for transfers, bridging, and x402 payments. Agent wallet transactions are gas-sponsored. Sponsorship is capped and subject to change. See [Fees](/agent-stack/agent-wallets/fees) for the full breakdown. # Agent Wallet Fees Source: https://developers.circle.com/agent-stack/agent-wallets/fees Fee breakdown for agent wallet operations, including bridging, swapping, and payments. The following fees may apply depending on the operations your agent wallet performs: | Fee | Amount | When it applies | | :--------------------- | :------------------------------- | :--------------------------------------------- | | Gas | \$0 (sponsored) | All onchain transactions | | CCTP fast transfer fee | Varies by source blockchain | Bridging | | Forwarding service fee | \$0.20 | Bridging | | Forwarding gas fee | Varies by destination blockchain | Bridging | | Swap provider fee | 2 bps | Swapping | | Gateway protocol fee | 0.5 bps | Crosschain x402 payments (free for same-chain) | | Eco deposit fee | Set by Eco | Gasless Gateway deposits using Eco | Eco is a third-party fast-deposit service that Circle does not operate or audit. Review [Eco's docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits) and test the flow before using it in production. What to know about agent wallet fees: * **Gas sponsorship**: Onchain transactions on agent wallets are gas-sponsored at no cost to you. Sponsorship is capped, subject to fair use, and may change over time. * **CCTP fast transfer**: Agent Wallets use CCTP fast transfer only for bridging, which provides near-instant finality at a higher fee than standard transfers. For fast transfer rates by source blockchain, see [CCTP fees](/cctp/concepts/fees). * **Forwarding gas fee**: Bridging also incurs a forwarding gas fee that varies by destination blockchain. # Quickstart: Create an Agent Wallet Source: https://developers.circle.com/agent-stack/agent-wallets/quickstart Get your first agent wallet set up in minutes. Create an agent wallet and fund it with USDC on Base. Prefer a guided setup? [Prompt your AI agent](/agent-stack/agent-wallets#get-started) to set it up. Your agent can only operate the wallet if it has access to the email address used during authentication. By default, only you receive the OTP. If you grant your agent access to your inbox, it can authenticate on your behalf and perform all wallet operations. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` ## Steps Follow these steps to create an agent wallet and fund it with USDC. ```bash theme={null} circle wallet login you@example.com ``` To use testnet, add `--testnet` to the command. Sessions are stored separately for mainnet and testnet and expire after 7 days. On first run, Circle CLI prompts you to accept the Terms of Use and Privacy Policy. Then Circle sends a one-time password to verify your identity. After authentication, agent wallets are created automatically on all supported blockchains. ```bash theme={null} circle wallet list --type agent --chain BASE ``` Copy the wallet address returned. You will use it in the steps below. Run `circle wallet fund` to add USDC to your wallet from another wallet you own. Replace `0xYourWalletAddress` with the address from the previous step: ```bash theme={null} circle wallet fund --address 0xYourWalletAddress --chain BASE --amount 10 --method crypto ``` The CLI prints a terminal QR code and an [EIP-681](https://eips.ethereum.org/EIPS/eip-681) deposit URI. Scan the QR code with your mobile wallet to send the funds. To buy USDC with a card instead, pass `--method fiat` to open the onramp provider in your browser. See [Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund) for all funding options. To use testnet, replace `BASE` with a testnet blockchain (for example, `ARC-TESTNET`) and omit `--method` and `--amount`. Testnet wallets are auto-funded with 20 USDC from the Circle faucet. See [supported blockchains](/agent-stack/agent-wallets/supported-blockchains) for the full list. Confirm the funds arrived. Replace `0xYourWalletAddress` with your wallet address: ```bash theme={null} circle wallet balance --address 0xYourWalletAddress --chain BASE ``` ## Next steps Now that you have a funded wallet, you can: * [Deposit for nanopayments](/agent-stack/agent-nanopayments/operations/deposit) and [pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service). * [Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer) to another address. * [Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge) to another blockchain. * [Swap tokens](/agent-stack/agent-wallets/wallet-operations/swap) using your agent wallet. * Explore [wallet operations](/agent-stack/agent-wallets/wallet-operations) for more tasks. # Agent Wallet Supported Blockchains Source: https://developers.circle.com/agent-stack/agent-wallets/supported-blockchains Blockchains supported by Agent Wallets on mainnet and testnet. Agent Wallets support the following blockchains on both mainnet and testnet, except Arc Testnet (testnet only). Pass the blockchain identifier to the `--chain` flag in CLI commands. Run `circle blockchain list` to retrieve the current list. | Blockchain | Mainnet identifier | Testnet identifier | | :---------- | :----------------: | :----------------: | | Arbitrum | `ARB` | `ARB-SEPOLIA` | | Arc Testnet | - | `ARC-TESTNET` | | Avalanche | `AVAX` | `AVAX-FUJI` | | Base | `BASE` | `BASE-SEPOLIA` | | Ethereum | `ETH` | `ETH-SEPOLIA` | | Monad | `MONAD` | `MONAD-TESTNET` | | Optimism | `OP` | `OP-SEPOLIA` | | Polygon PoS | `MATIC` | `MATIC-AMOY` | | Unichain | `UNI` | `UNI-SEPOLIA` | # Wallet Operations Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations Tasks you can perform with an agent wallet using Circle CLI, including transfers, bridging, payments, and more. Wallet operations are tasks you can perform with an agent wallet using [Circle CLI](/agent-stack/circle-cli). Full command syntax is in the [CLI Command Reference](/agent-stack/circle-cli/command-reference). The following table describes common wallet operations and the CLI commands to perform them. | Operation | What it does | Command | | :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :------------------------ | | [Authenticate](/agent-stack/agent-wallets/wallet-operations/authenticate) | Sign up or log in to your agent wallet using email OTP. Creates a session valid for 7 days. | `circle wallet login` | | [Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund) | Add funds to your agent wallet using a wallet transfer or fiat onramp. | `circle wallet fund` | | [Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer) | Send USDC and other tokens to a designated wallet address. | `circle wallet transfer` | | [Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge) | Move USDC from one blockchain to another using CCTP. | `circle bridge transfer` | | [Swap tokens](/agent-stack/agent-wallets/wallet-operations/swap) | Swap one token for another directly from your agent wallet. | `circle wallet swap` | | [Execute contract](/agent-stack/agent-wallets/wallet-operations/execute-contract) | Interact with a smart contract by calling a write function. | `circle wallet execute` | | [Sign messages](/agent-stack/agent-wallets/wallet-operations/sign) | Sign a message or EIP-712 typed data with your wallet. | `circle wallet sign` | | [Set policies](/agent-stack/agent-wallets/wallet-operations/custom-policies) | Set USDC transfer limits or allow/block specific recipient or contract addresses. | `circle wallet limit set` | For deposits into Gateway, payments to x402-compatible services, and withdrawals, see [Agent Nanopayments](/agent-stack/agent-nanopayments). # How to: Authenticate an Agent Wallet Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/authenticate Log in with your email to create an agent wallet session on all supported blockchains. Authenticating creates an agent session and provisions agent wallets on all [supported blockchains](/agent-stack/agent-wallets/supported-blockchains) automatically. You only need to do this once per environment. Sessions last 7 days. This is a prerequisite for all other wallet operations, including [Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund), [Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer), [Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge), and [Pay for service](/agent-stack/agent-nanopayments/operations/pay-for-service). Your agent can only operate the wallet if it has access to the email address used during authentication. By default, only you receive the OTP. If you grant your agent access to your inbox, it can authenticate on your behalf and perform all wallet operations. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` ## Steps Choose the flow that matches your environment. Use **Interactive** when you can respond to prompts in a terminal. Use **Non-interactive** for scripts and AI agents that can't respond to interactive prompts. Run `circle wallet login` with your email address: ```bash theme={null} circle wallet login you@example.com ``` To use testnet, add `--testnet` to the command. Sessions are stored separately for mainnet and testnet and expire after 7 days. On first run, Circle CLI prompts you to accept the Terms of Use and Privacy Policy. Check your email for the one-time password from Circle. Enter it in the terminal to verify your identity. You should see output similar to: ```text theme={null} Logged in as you@example.com ``` Agent wallets are created automatically on all [supported blockchains](/agent-stack/agent-wallets/supported-blockchains). List your wallets to find the address for the blockchain you want to use: ```bash theme={null} circle wallet list --type agent --chain BASE ``` Copy the address returned. You'll need it for [funding your wallet](/agent-stack/agent-wallets/wallet-operations/fund), [transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer), [depositing for nanopayments](/agent-stack/agent-nanopayments/operations/deposit), and other operations. Run `circle wallet login` with `--init` to send the OTP and capture a request ID. The `CIRCLE_ACCEPT_TERMS=1` prefix accepts the Circle CLI Terms of Use and Privacy Policy so the command doesn't pause for input on first run: ```bash theme={null} CIRCLE_ACCEPT_TERMS=1 circle wallet login you@example.com --init ``` The CLI prints a request ID. Request IDs expire after 10 minutes and are consumed on first use. Pass the request ID and the OTP from your inbox: ```bash theme={null} circle wallet login --request --otp B1X-123456 ``` OTP codes are alphanumeric. Once login completes, agent wallets are created automatically on all [supported blockchains](/agent-stack/agent-wallets/supported-blockchains). List your wallets to find the address for the blockchain you want to use: ```bash theme={null} circle wallet list --type agent --chain BASE ``` Copy the address returned. You'll need it for [funding your wallet](/agent-stack/agent-wallets/wallet-operations/fund), [transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer), [depositing for nanopayments](/agent-stack/agent-nanopayments/operations/deposit), and other operations. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options. # How to: Bridge USDC Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/bridge Move USDC from one blockchain to another using CCTP. Bridge USDC across blockchains using [CCTP](/cctp). Circle handles attestation and minting on the destination chain. You don't need a funded wallet or gas on the destination. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Steps Follow these steps to bridge USDC to another blockchain. Get the estimated fee before bridging: ```bash theme={null} circle bridge get-fee ARB --chain BASE ``` Run `circle bridge transfer` with the destination chain, amount, and your wallet details: ```bash theme={null} circle bridge transfer ARB --amount 10.0 --address 0xYourWalletAddress --chain BASE ``` USDC is burned on the source chain and minted on the destination. The command returns once the bridge is complete: ```json theme={null} { "data": { "message": "Bridge complete: 10.0 USDC from BASE to ARB", "burnTxHash": "0xabc...", "forwardTxHash": "0xdef...", "fromChain": "BASE", "toChain": "ARB" } } ``` If the bridge is still processing, you can check its status: ```bash theme={null} circle bridge status 0xabc... --chain BASE ``` See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including specifying a different recipient address on the destination chain. # How to: Set Spending Policies Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/custom-policies Set transfer limits or address allowlists and blocklists on your agent wallet to control how it can move USDC and interact with contracts. Spending policies let you cap your agent wallet's USDC transfers over a rolling time window, or restrict the wallet to specific recipient or contract addresses. Policies apply to mainnet agent wallets only. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate) your agent wallet. Spending policies require a mainnet agent wallet. Testnet is not supported. Setting a policy triggers a second email OTP to confirm the change. The OTP is used once and not stored. ## Steps Choose the policy type that matches your goal. Run `circle wallet limit set` with per-transaction, daily, weekly, and monthly caps: ```bash theme={null} circle wallet limit set \ --address 0xYourWalletAddress \ --chain BASE \ --policy-type stablecoin \ --per-tx 100 \ --daily 500 \ --weekly 2000 \ --monthly 5000 ``` Circle sends an email OTP to your agent session email to confirm the policy change. Limits must satisfy: per-transaction ≤ daily ≤ weekly ≤ monthly. Confirm the limits are in effect: ```bash theme={null} circle wallet limit --address 0xYourWalletAddress --chain BASE ``` Allowlists and blocklists restrict your wallet to specific recipient or contract addresses. Run `circle wallet limit set` with `--rule-type` and a bracketed, comma-separated `--targets` list of EVM addresses. The example below blocks transfers to two recipient addresses: ```bash theme={null} circle wallet limit set \ --address 0xYourWalletAddress \ --chain BASE \ --policy-type stablecoin \ --rule-type recipient-blocklist \ --targets "[0xBAD1,0xBAD2]" ``` `--rule-type` accepts: * `recipient-allowlist` / `recipient-blocklist`: allow or block USDC transfers to specific addresses. * `contract-allowlist` / `contract-blocklist`: allow or block contract interactions with specific addresses. Circle sends an email OTP to your agent session email to confirm the policy change. Confirm the rule is in effect: ```bash theme={null} circle wallet limit --address 0xYourWalletAddress --chain BASE ``` See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options. # How to: Execute a Smart Contract Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/execute-contract Call a write function on a smart contract from your agent wallet. Execute write functions on any smart contract from your agent wallet. Common uses include approving token allowances, interacting with DeFi protocols, or calling custom contract logic. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Steps Follow these steps to execute a smart contract function. For Circle contracts (USDC, CCTP, Gateway), look up the address for your blockchain: ```bash theme={null} circle contract address usdc --chain BASE ``` Run `circle wallet execute` with the ABI function signature, parameters, and contract address: ```bash theme={null} circle wallet execute "approve(address,uint256)" 0xSpender 1000000 \ --contract 0xUSDC \ --address 0xYourWalletAddress \ --chain BASE ``` The CLI waits for the transaction to reach a terminal state and returns the result: ```json theme={null} { "data": { "id": "abc-123-...", "state": "CONFIRMED", "blockchain": "BASE", "txHash": "0xabc...", "operation": "CONTRACT_EXECUTION", "contractAddress": "0xUSDC", "abiFunctionSignature": "approve(address,uint256)" } } ``` If the transaction fails, the CLI prints the reason. Verify the contract address, function signature, and parameters, then retry. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including `--amount` to send native token value with the call. # How to: Fund an Agent Wallet Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/fund Add USDC to your agent wallet using a wallet transfer or fiat onramp. Add USDC to your agent wallet before sending transfers or paying for services. This is a prerequisite for [transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer), [bridging USDC](/agent-stack/agent-wallets/wallet-operations/bridge), [paying for services](/agent-stack/agent-nanopayments/operations/pay-for-service), and other operations that spend USDC. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate) your agent wallet. ## Steps Run `circle wallet fund` with your wallet address, blockchain, amount, and method (crypto or fiat). Replace `0xYourWalletAddress` with your wallet address. ```bash theme={null} circle wallet fund --address 0xYourWalletAddress \ --chain BASE --amount 10 --method crypto ``` The CLI prints a terminal QR code and an [EIP-681](https://eips.ethereum.org/EIPS/eip-681) deposit URI. Scan the QR code with your mobile wallet to send the funds. Pass `--export ` to save a PNG instead, or `--open` to render the QR code in a browser tab. ```bash theme={null} circle wallet fund --address 0xYourWalletAddress \ --chain BASE --amount 10 --method fiat ``` Your browser opens to the onramp provider where you can purchase USDC with a card or bank transfer. Pass `--no-open` to print the URL instead of opening the browser. To use testnet, replace `BASE` with a testnet blockchain (for example, `ARC-TESTNET`) and omit `--method` and `--amount`. Testnet wallets are auto-funded with 20 USDC from the Circle faucet. See [supported blockchains](/agent-stack/agent-wallets/supported-blockchains) for the full list. Check your balance to confirm the deposit: ```bash theme={null} circle wallet balance --address 0xYourWalletAddress --chain BASE ``` See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including `--token` to fund with ETH or native tokens. # How to: Sign a Message Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/sign Sign a plain text message or EIP-712 typed data with your agent wallet. Sign messages or EIP-712 typed data with your agent wallet. Signing is commonly used to prove wallet ownership or authorize offchain actions. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate) your agent wallet. ## Sign a message Run `circle wallet sign` with your message and wallet details. Each command returns a signature (`0xabcdef1234...`): ```bash theme={null} # Sign a plain text message circle wallet sign message "hello world" --address 0xYourWalletAddress --chain BASE # Sign a hex-encoded message circle wallet sign message "0xdeadbeef" --hex --address 0xYourWalletAddress --chain BASE # Sign EIP-712 typed data (EVM only) circle wallet sign typed-data \ '{"types":{...},"primaryType":"Mail","domain":{...},"message":{...}}' \ --address 0xYourWalletAddress \ --chain BASE ``` Typed data signing is supported on EVM blockchains only. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options. # How to: Swap Tokens Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/swap Swap one token for another from your agent wallet. Swap tokens from your agent wallet using `circle wallet swap`. Optionally get a price quote first to verify the expected output before committing funds. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Steps Run `circle wallet swap` with `--quote` to see the estimated output without executing the swap: ```bash theme={null} circle wallet swap EURC 10 USDC --chain BASE --quote ``` ```json theme={null} { "data": { "message": "Quote: 10 EURC → ~9.95 USDC (min 0.000001) on BASE", "sellToken": "EURC", "sellAmount": "10", "buyToken": "USDC", "chain": "BASE", "estimatedOutput": "9.95", "stopLimit": "0.000001" } } ``` Run `circle wallet swap` with your wallet address and a `` stop-limit. If the estimated output falls below this value onchain, the swap fails instead of settling at an unfavorable rate: ```bash theme={null} circle wallet swap EURC 10 USDC 9.9 --address 0xYourWalletAddress --chain BASE ``` ```json theme={null} { "data": { "message": "Swap complete: 10 EURC → min 9.9 USDC on BASE", "sellToken": "EURC", "sellAmount": "10", "buyToken": "USDC", "buyMin": "9.9", "chain": "BASE" } } ``` See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including `--slippage-bps` to set maximum slippage. # How to: Transfer USDC Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/transfer Send USDC from your agent wallet to another address on a supported blockchain. The transfer confirms onchain before the command returns, so no manual polling is required. ## Prerequisites Before you begin, ensure you have: * Installed [Node.js v20.18.2 or later](https://nodejs.org/). * Installed Circle CLI: ```bash theme={null} npm install -g @circle-fin/cli ``` * Completed the [Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart) (authenticates and funds your wallet). ## Send USDC Run `circle wallet transfer` with the recipient address, amount, and your wallet details: ```bash theme={null} circle wallet transfer 0xRecipient --amount 1.0 --address 0xYourWalletAddress --chain BASE ``` The command returns the result once the transaction reaches a terminal state: ```json theme={null} { "data": { "id": "abc-123-...", "state": "CONFIRMED", "blockchain": "BASE", "txHash": "0xabc...", "sourceAddress": "0xYourWalletAddress", "destinationAddress": "0xRecipient", "amounts": ["1"], "operation": "TRANSFER" } } ``` If the transfer fails, the command prints the reason. Check your wallet balance and retry. See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for full syntax and options, including `--token` to transfer tokens other than USDC. # Circle CLI Source: https://developers.circle.com/agent-stack/circle-cli A command-line tool to access Circle's product suite, including agent wallets, x402-compatible payments, and crosschain transfers. Circle CLI gives developers and AI agents a unified interface for onchain operations. Create and manage wallets, transfer, swap, and bridge USDC across blockchains, execute smart contracts, and discover or pay for services. All through a single command interface, without integrating multiple APIs and SDKs. Use Circle CLI to access: * **[Agent Wallets](/agent-stack/agent-wallets)**: Wallets with email OTP authentication, full onchain capabilities, and customizable policy enforcement. * **Local wallets**: Self-custodial wallets imported from a private key or mnemonic, stored using the [Open Wallet Standard](https://github.com/open-wallet-standard/core). * **Circle products**: [CCTP](/cctp) for USDC bridging and [Gateway](/gateway) for [x402 nanopayments](/gateway/nanopayments). ## Installation Install Circle CLI from [npm](https://www.npmjs.com/) (requires [Node.js v20.18.2 or later](https://nodejs.org/)): ```bash theme={null} npm install -g @circle-fin/cli ``` Verify the installation: ```bash theme={null} circle --version ``` Once installed, you can run any [Circle CLI command](/agent-stack/circle-cli/command-reference). ## Get started Create an agent wallet and fund it with USDC. All Circle CLI commands, options, and examples. # CLI Command Reference Source: https://developers.circle.com/agent-stack/circle-cli/command-reference Complete command reference for Circle's agent command-line tool, organized by resource. Circle CLI follows a `circle [options]` pattern. All commands support `--help` for inline documentation. ## Global options | Option | Description | | :-------------------------- | :------------------------------------- | | `--output json\|text\|yaml` | Set output format. Defaults to `text`. | | `-q, --quiet` | Minimal output, suitable for piping. | | `-h, --help` | Show help for any command. | | `-v, --version` | Print the Circle CLI version and exit. | *** ## Wallet commands Manage your [agent wallet](/agent-stack/agent-wallets). ### `circle wallet login` Authenticate using email OTP to create or access an agent wallet session. **Syntax** ```bash theme={null} circle wallet login [options] circle wallet login --init circle wallet login --request --otp ``` **Options** | Option | Description | | :--------------- | :------------------------------------------------------------------------------------------------------------------- | | `--type` | Wallet type. Defaults to `agent`. | | `--testnet` | Authenticate against testnet. Sessions are stored separately from mainnet. | | `--init` | Two-step login for scripts and AI agents. Sends the OTP and returns a request ID. Pair with `--request`. | | `--request ` | Complete a `--init` login. Combine with `--otp `. | | `--otp ` | One-time password for the request ID. Required with `--request`. Codes are alphanumeric (for example, `B1X-123456`). | **Examples** ```bash theme={null} # Interactive login (mainnet) circle wallet login you@example.com # Two-step login for scripts and AI agents circle wallet login you@example.com --init circle wallet login --request --otp B1X-123456 ``` Request IDs from `--init` are stored at `~/.circle/login-requests/.json`, expire after 10 minutes, and are deleted after a successful `--request`. *** ### `circle wallet logout` Clear stored credentials for the current session. **Syntax** ```bash theme={null} circle wallet logout [options] ``` **Options** | Option | Description | | :------- | :------------------------------------------------------------------- | | `--type` | Clear credentials for a specific wallet type (for example, `agent`). | **Example** ```bash theme={null} circle wallet logout --type agent ``` *** ### `circle wallet status` Show the current authentication status and session details. **Syntax** ```bash theme={null} circle wallet status [options] ``` **Options** | Option | Description | | :------- | :------------------------------------------------------------- | | `--type` | Show status for a specific wallet type (for example, `agent`). | **Example** ```bash theme={null} circle wallet status --type agent ``` *** ### `circle wallet create` Create an additional wallet, separate from the wallets provisioned during login. Each user can have at most 5 agent wallets. **Syntax** ```bash theme={null} circle wallet create [options] ``` **Options** | Option | Description | | :------------------ | :-------------------------------------------------------- | | `--type` | Wallet type: `agent` (default) or `local`. | | `--testnet` | Create a testnet wallet. Omit for mainnet. | | `--idempotency-key` | Unique key to prevent duplicate wallet creation on retry. | **Example** ```bash theme={null} circle wallet create --type agent --testnet ``` *** ### `circle wallet list` List wallets associated with your account. **Syntax** ```bash theme={null} circle wallet list --chain [options] ``` **Options** | Option | Description | | :-------- | :----------------------------------------- | | `--chain` | Blockchain to list wallets on. | | `--type` | Filter by wallet type: `agent` or `local`. | **Example** ```bash theme={null} circle wallet list --chain ARC-TESTNET --type agent ``` *** ### `circle wallet limit` Show spending policy limits for an agent wallet. Mainnet only. **Syntax** ```bash theme={null} circle wallet limit --address --chain ``` **Options** | Option | Description | | :---------- | :------------------------------------------------ | | `--address` | Agent wallet address. | | `--chain` | Mainnet blockchain. Testnet chains not supported. | **Examples** ```bash theme={null} circle wallet limit --address 0x... --chain BASE circle wallet limit --address 0x... --chain BASE --output json ``` *** ### `circle wallet limit set` Set a custom spending policy for an agent wallet. Requires a second email OTP to confirm the change. Mainnet only. Use `--rule-type` to choose the kind of policy: * `transfer-limit` (default): cap how much USDC the wallet can transfer per transaction or over a rolling time window. Set with `--per-tx`, `--daily`, `--weekly`, `--monthly`. * `recipient-allowlist` / `recipient-blocklist`: allow or block transfers to specific recipient addresses. Set with `--targets`. * `contract-allowlist` / `contract-blocklist`: allow or block contract interactions with specific addresses. Set with `--targets`. **Syntax** ```bash theme={null} # Transfer limit (default) circle wallet limit set --address --chain \ --policy-type \ [--per-tx ] [--daily ] [--weekly ] [--monthly ] \ [options] # Allowlist or blocklist circle wallet limit set --address --chain \ --policy-type \ --rule-type \ --targets "[0xAddr1,0xAddr2]" \ [options] ``` **Options** | Option | Description | | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | | `--address` | Agent wallet address. | | `--chain` | Mainnet blockchain. Testnet blockchains are not supported. | | `--policy-type` | Policy category: `stablecoin` for transfer-based rules, `contract` for contract-based rules. Auto-set when `--rule-type` is a contract rule. | | `--rule-type` | Rule shape: `transfer-limit` (default), `recipient-allowlist`, `recipient-blocklist`, `contract-allowlist`, or `contract-blocklist`. | | `--per-tx` | Per-transaction spending cap. Used with `--rule-type transfer-limit`. | | `--daily` | Daily rolling-window cap. Used with `--rule-type transfer-limit`. | | `--weekly` | Weekly rolling-window cap. Used with `--rule-type transfer-limit`. | | `--monthly` | Monthly rolling-window cap. Used with `--rule-type transfer-limit`. | | `--targets` | Bracketed comma-separated list of EVM addresses (for example, `"[0xA,0xB]"`). Required for allowlist and blocklist rule types. Validated client-side. | | `--email` | Email address for the confirmation OTP. Defaults to the agent session email. | Transfer limits must satisfy: per-transaction ≤ daily ≤ weekly ≤ monthly. **Examples** ```bash theme={null} # Transfer limits circle wallet limit set \ --address 0x... \ --chain BASE \ --policy-type stablecoin \ --per-tx 100 \ --daily 500 \ --weekly 2000 \ --monthly 5000 # Recipient blocklist circle wallet limit set \ --address 0x... \ --chain BASE \ --policy-type stablecoin \ --rule-type recipient-blocklist \ --targets "[0xBAD1,0xBAD2]" ``` *** ### `circle wallet limit reset` Reset all custom spending policies for an agent wallet back to defaults. Requires a second email OTP to confirm. Mainnet only. **Syntax** ```bash theme={null} circle wallet limit reset --address --chain [options] ``` **Options** | Option | Description | | :------------ | :------------------------------------------------ | | `--address` | Agent wallet address. | | `--chain` | Mainnet blockchain. Testnet chains not supported. | | `--yes`, `-y` | Skip the confirmation prompt. | **Example** ```bash theme={null} circle wallet limit reset --address 0x... --chain BASE --yes ``` *** ### `circle wallet limit budget` Show remaining spending budgets for an agent wallet. Displays per-transaction limits and rolling-window remaining amounts (daily, weekly, monthly). Budgets are EVM-wide and not blockchain-specific. Mainnet only. **Syntax** ```bash theme={null} circle wallet limit budget --address ``` **Options** | Option | Description | | :---------- | :-------------------- | | `--address` | Agent wallet address. | **Example** ```bash theme={null} circle wallet limit budget --address 0x... ``` *** ### `circle wallet balance` Show the token balance for a wallet address on a given blockchain. **Syntax** ```bash theme={null} circle wallet balance --address --chain [options] ``` **Options** | Option | Description | | :---------- | :------------------------------------------------------------------------------ | | `--address` | Wallet address. | | `--chain` | Blockchain. | | `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. | **Example** ```bash theme={null} circle wallet balance --address 0x... --chain BASE ``` *** ### `circle wallet fund` Add funds to a wallet by transfer from another wallet (crypto), through a fiat onramp, or from the testnet faucet. **Syntax** ```bash theme={null} circle wallet fund --address --chain --amount --method [options] ``` **Options** | Option | Description | | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | | `--address` | Wallet address to fund. | | `--chain` | Blockchain. | | `--amount` | Amount to fund. Ignored on testnet. | | `--token` | Token: `usdc` (default), `eth`, `native`. | | `--method` | Funding method: `crypto` or `fiat`. Required on mainnet. Omit on testnet, where wallets auto-fund from the Circle faucet. | | `--export ` | With `--method crypto`, write a PNG QR code into `` instead of printing it to the terminal. | | `--open` | Open the result in your browser. With `--method fiat`, opens the onramp provider. With `--method crypto`, opens an HTML page with the QR code. | | `--no-open` | Print the onramp URL without opening it. Used with `--method fiat` only. | **Examples** ```bash theme={null} # Fund with crypto from another wallet circle wallet fund --address 0x... --chain BASE --amount 10 --method crypto # Fund with fiat through the onramp provider circle wallet fund --address 0x... --chain BASE --amount 10 --method fiat # Testnet auto-funded from faucet (omit --method and --amount) circle wallet fund --address 0x... --chain ARC-TESTNET ``` *** ### `circle wallet transfer` Transfer tokens from a wallet to another address. **Syntax** ```bash theme={null} circle wallet transfer --amount --address --chain [options] ``` **Arguments** | Argument | Description | | :------------ | :----------------- | | `` | Recipient address. | **Options** | Option | Description | | :----------- | :------------------------------------------------------------------------------ | | `--amount` | Amount to transfer. | | `--address` | Source wallet address. | | `--chain` | Blockchain. | | `--token` | Token contract address. Omit to use USDC. | | `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. | | `--estimate` | Show estimated fees without submitting the transfer. | **Example** ```bash theme={null} circle wallet transfer 0xRecipient --amount 5.0 --address 0x... --chain ARC-TESTNET ``` *** ### `circle wallet swap` Swap one token for another. Requires an agent wallet. Arc Testnet is the only testnet supported. **Syntax** ```bash theme={null} circle wallet swap [] --address --chain [options] ``` **Arguments** | Argument | Description | | :-------------- | :--------------------------------------------------------------------- | | `` | Token to sell. Use a symbol (for example, `EURC`) or contract address. | | `` | Amount to sell. | | `` | Token to buy. Use a symbol (for example, `USDC`) or contract address. | | `[]` | Minimum acceptable output (stop-limit). Omit when using `--quote`. | **Options** | Option | Description | | :------------------ | :---------------------------------------------------------------------------------------------- | | `--address` | Agent wallet address. Optional when using `--quote`. | | `--chain` | Blockchain. | | `--quote` | Get a price quote without executing the swap. Does not require wallet ownership or `buyAmount`. | | `--slippage-bps` | Maximum slippage in basis points (for example, `50` = 0.5%). | | `--idempotency-key` | Unique key to prevent duplicate swaps on retry. | **Examples** ```bash theme={null} circle wallet swap EURC 100 USDC 99.5 --address 0x... --chain ARC-TESTNET circle wallet swap EURC 100 USDC --chain ARC-TESTNET --quote ``` *** ### `circle wallet sign message` Sign a plain text or hex-encoded message with your wallet. **Syntax** ```bash theme={null} circle wallet sign message --address --chain [options] ``` **Arguments** | Argument | Description | | :---------- | :----------------------------------- | | `` | Message to sign (plain text or hex). | **Options** | Option | Description | | :---------- | :--------------------------------------------- | | `--address` | Wallet address. | | `--chain` | Blockchain. | | `--hex` | Message is hex-encoded (must start with `0x`). | **Example** ```bash theme={null} circle wallet sign message "hello world" --address 0x... --chain ARC-TESTNET ``` *** ### `circle wallet sign typed-data` Sign EIP-712 typed data with your wallet. **Syntax** ```bash theme={null} circle wallet sign typed-data --address --chain ``` **Arguments** | Argument | Description | | :------- | :----------------------------------- | | `` | EIP-712 typed data as a JSON string. | **Options** | Option | Description | | :---------- | :-------------- | | `--address` | Wallet address. | | `--chain` | Blockchain. | **Example** ```bash theme={null} circle wallet sign typed-data '{"types":{...},"primaryType":"Mail","domain":{...},"message":{...}}' \ --address 0x... \ --chain ARC-TESTNET ``` *** ### `circle wallet execute` Execute a smart contract write function from a wallet. **Syntax** ```bash theme={null} circle wallet execute [...] \ --contract \ --address \ --chain \ [options] ``` **Arguments** | Argument | Description | | :----------------------- | :---------------------------------------------------------------- | | `` | ABI function signature (for example, `approve(address,uint256)`). | | `[...]` | ABI parameters, space-separated. | **Options** | Option | Description | | :----------- | :------------------------------------------------------------------------------ | | `--contract` | Contract address. | | `--address` | Wallet address. | | `--chain` | Blockchain. | | `--amount` | Native token value to send with the call. Defaults to `0`. | | `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. | | `--estimate` | Show estimated fees without submitting the transaction. | **Example** ```bash theme={null} circle wallet execute "approve(address,uint256)" 0xSpender 1000000 \ --contract 0xUSDC \ --address 0x... \ --chain ARC-TESTNET ``` *** ### `circle wallet import` Import a local wallet from a private key or mnemonic phrase. Stored using the Open Wallet Standard at `~/.ows/wallets/`. Local wallets bypass Circle's compliance and safety controls. Spending policies, OFAC screening, and audit logging only apply to agent wallets. **Syntax** ```bash theme={null} circle wallet import [--private-key | --mnemonic] ``` **Options** | Option | Description | | :-------------- | :----------------------------------------------------------------------- | | `--private-key` | Import from a private key. You'll be prompted to enter the key securely. | | `--mnemonic` | Import from a mnemonic phrase. You'll be prompted to enter it securely. | Do not pass your private key or mnemonic as a command-line argument or environment variable in plain text. Enter it at the prompt or use a secrets manager. **Example** ```bash theme={null} circle wallet import my-wallet --private-key ``` *** ## Services commands Discover and pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API services. ### `circle services search` Search for available services by keyword. Omit the query to list all services. **Syntax** ```bash theme={null} circle services search [] [options] ``` **Arguments** | Argument | Description | | :---------- | :----------------------------------------------------------- | | `[]` | Optional search keyword or phrase. Omit to list all results. | **Options** | Option | Description | | :----------- | :----------------------------------------------------------------------------- | | `--category` | Filter by category (for example, `FINANCIAL_ANALYSIS`, `WEB_SEARCH_RESEARCH`). | | `--type` | Filter by service type. | | `--limit` | Maximum number of results to return. Defaults to `50`. | | `--offset` | Number of results to skip, for pagination. Defaults to `0`. | **Example** ```bash theme={null} circle services search "weather" --category WEB_SEARCH_RESEARCH --limit 20 ``` *** ### `circle services inspect` Inspect the payment requirements for a service URL. The CLI auto-detects the HTTP method from the service's discovery metadata and auto-generates a minimal request body from its input schema. Override either with the flags below. **Syntax** ```bash theme={null} circle services inspect [options] ``` **Arguments** | Argument | Description | | :------- | :---------------------- | | `` | Service URL to inspect. | **Options** | Option | Description | | :--------------- | :----------------------------------------------------------------------- | | `--method`, `-X` | HTTP method override: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. | | `--data`, `-d` | Request body as a JSON string. Overrides the auto-generated body. | | `--header`, `-H` | Custom request header as `Key: Value`. Repeat the flag to send multiple. | **Example** ```bash theme={null} circle services inspect https://api.example.com/weather -X POST -d '{"city":"SF"}' ``` *** ### `circle services pay` Pay for a service using your agent wallet. **Syntax** ```bash theme={null} circle services pay --address
--chain [options] ``` **Arguments** | Argument | Description | | :------- | :---------------------- | | `` | Service URL to pay for. | **Options** | Option | Description | | :-------------------- | :----------------------------------------------------------------------- | | `--address` | Agent wallet address. | | `--chain` | Blockchain to pay from. | | `--max-amount ` | Refuse to pay more than this amount in USDC (for example, `0.01`). | | `--method`, `-X` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. Defaults to `GET`. | | `--data`, `-d` | Request body as a JSON string. | | `--header`, `-H` | Custom request header as `Key: Value`. | | `--estimate` | Show payment requirements without submitting payment. | | `--quiet`, `-q` | Print response body only (useful for piping). | | `--timeout ` | Per-step timeout in seconds. Defaults to `30`. | **Example** ```bash theme={null} circle services pay https://api.example.com/weather --address 0x... --chain BASE ``` Failed payments write debug logs to `~/.circle-cli/payments/`. Check the most recent file for the request, response, and stage where the failure occurred. *** ## Bridge commands Bridge USDC across blockchains using [CCTP](/cctp). ### `circle bridge transfer` Bridge USDC from one blockchain to another. **Syntax** ```bash theme={null} circle bridge transfer [] --amount --address --chain ``` **Arguments** | Argument | Description | | :-------------- | :-------------------------------------------------------------------------- | | `` | Destination blockchain (for example, `ARB`, `ETH`). | | `[]` | Recipient address on the destination. Defaults to the value of `--address`. | **Options** | Option | Description | | :------------------ | :-------------------------------------------------- | | `--amount` | USDC amount the recipient will receive. | | `--address` | Sender wallet address. | | `--chain` | Source blockchain. | | `--rpc-url` | RPC endpoint override for the source blockchain. | | `--idempotency-key` | Unique key to prevent duplicate transfers on retry. | | `--quiet`, `-q` | Print transaction hash only (useful for piping). | **Example** ```bash theme={null} circle bridge transfer ARB-SEPOLIA --amount 10.0 --address 0x... --chain ARC-TESTNET ``` *** ### `circle bridge status` Check the status of a bridge transfer by transaction hash. **Syntax** ```bash theme={null} circle bridge status --chain ``` **Arguments** | Argument | Description | | :--------- | :---------------------------------------- | | `` | Transaction hash of the burn transaction. | **Options** | Option | Description | | :-------- | :----------------- | | `--chain` | Source blockchain. | **Example** ```bash theme={null} circle bridge status 0xabc... --chain ARC-TESTNET ``` *** ### `circle bridge get-fee` Get the estimated fee for bridging from a given blockchain. **Syntax** ```bash theme={null} circle bridge get-fee --chain ``` **Arguments** | Argument | Description | | :------- | :-------------------------------------------------- | | `` | Destination blockchain (for example, `ARB`, `ETH`). | **Options** | Option | Description | | :-------- | :----------------- | | `--chain` | Source blockchain. | **Example** ```bash theme={null} circle bridge get-fee ETH --chain ARC-TESTNET ``` *** ## Gateway commands Interact with [Circle Gateway](/gateway). ### `circle gateway balance` Show your Gateway balance for nanopayments. **Syntax** ```bash theme={null} circle gateway balance --address --chain [options] ``` **Options** | Option | Description | | :---------- | :------------------------------------------------------------------------------------------------------------------------- | | `--address` | Wallet address. | | `--chain` | Blockchain where the wallet lives. Any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. | | `--all` | Show all blockchains including those with zero balances. | | `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. | **Examples** ```bash theme={null} circle gateway balance --address 0x... --chain BASE circle gateway balance --address 0x... --chain BASE --all ``` *** ### `circle gateway deposit` Deposit USDC into Circle Gateway for nanopayments. Minimum deposit is `0.5` USDC. **Syntax** ```bash theme={null} circle gateway deposit --amount --address --chain --method [options] ``` **Options** | Option | Description | | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--amount` | Amount of USDC to deposit. | | `--address` | Wallet address. | | `--chain` | Source blockchain. With `--method direct`, any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. With `--method eco`, only `BASE` and `BASE-SEPOLIA`. | | `--method` | Deposit method: `eco` or `direct`. Eco deposits always settle on Polygon (`MATIC` mainnet, `MATIC-AMOY` testnet) regardless of source. | | `--timeout` | Transaction poll timeout in seconds. Defaults to `120`. | Eco is a third-party fast-deposit service that Circle does not operate or audit. Review [Eco's docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits) and test the flow before using it in production. **Example** ```bash theme={null} circle gateway deposit --amount 5 --address 0x... --chain BASE --method eco ``` *** ### `circle gateway withdraw` Withdraw USDC from Circle Gateway back to a wallet on the same blockchain. **Syntax** ```bash theme={null} circle gateway withdraw --amount --address --chain [options] ``` **Options** | Option | Description | | :------------ | :--------------------------------------------------------------------------------------------------------- | | `--amount` | Amount of USDC to withdraw. | | `--address` | Source wallet address (the Gateway depositor). | | `--chain` | Source blockchain. Any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. | | `--recipient` | Destination address to receive USDC. Defaults to `--address`. | | `--timeout` | Mint transaction poll timeout in seconds. Defaults to `120`. Agent wallets only. | Withdrawals are same-chain only. The withdrawn USDC is minted on `--chain`. Agent wallets must be Smart Contract Accounts (SCAs). Pass the SCA address as `--address`. JSON output includes `transferId`, `estimatedFee`, and `chargedFee`. **Examples** ```bash theme={null} # Withdraw to your own wallet circle gateway withdraw --amount 0.1 --address 0x... --chain BASE # Withdraw to a different recipient circle gateway withdraw --amount 5 --address 0x... --chain BASE --recipient 0xOTHER ``` *** ## Blockchain commands Query supported blockchain information. ### `circle blockchain list` List all blockchains supported by Circle CLI. **Syntax** ```bash theme={null} circle blockchain list ``` *** ### `circle blockchain config` Show or update the RPC URL for a blockchain. **Syntax** ```bash theme={null} circle blockchain config --chain [options] ``` **Options** | Option | Description | | :---------- | :------------------------------------------------------------- | | `--chain` | Blockchain to configure. | | `--rpc-url` | Set a custom RPC URL override for this blockchain. | | `--default` | Reset to the default RPC URL. Cannot be used with `--rpc-url`. | **Examples** ```bash theme={null} circle blockchain config --chain ARC-TESTNET --output json circle blockchain config --chain ARC-TESTNET --rpc-url https://my-node.example.com circle blockchain config --chain ARC-TESTNET --default ``` *** ## Transaction commands Manage pending and submitted transactions. ### `circle transaction list` List transaction history for a wallet. **Syntax** ```bash theme={null} circle transaction list --address --chain [options] ``` **Options** | Option | Description | | :--------------- | :---------------------------------------------------------------------------------------------------------------------------- | | `--address` | Wallet address. | | `--chain` | Blockchain. | | `--operation` | Filter by operation: `transfer` or `execute`. | | `--state` | Filter by state: `initiated`, `queued`, `sent`, `confirmed`, `complete`, `failed`, `cancelled`, `denied`, `cleared`, `stuck`. | | `--tx-type` | Filter by direction: `inbound` or `outbound`. | | `--lowest-nonce` | Return only the lowest-nonce pending transaction. Ignores other filters. | | `--cursor` | Pagination token: return transactions after this ID. | | `--limit` | Maximum number of transactions to return. Defaults to `50`. | **Examples** ```bash theme={null} circle transaction list --address 0x... --chain ARC-TESTNET circle transaction list --address 0x... --chain ARC-TESTNET --operation transfer --state confirmed ``` *** ### `circle transaction cancel` Cancel a pending transaction. **Syntax** ```bash theme={null} circle transaction cancel --address --chain ``` **Arguments** | Argument | Description | | :------- | :-------------- | | `` | Transaction ID. | **Options** | Option | Description | | :---------- | :-------------- | | `--address` | Wallet address. | | `--chain` | Blockchain. | **Example** ```bash theme={null} circle transaction cancel abc-123 --address 0x... --chain ARC-TESTNET ``` *** ### `circle transaction accelerate` Accelerate a pending transaction by increasing the gas fee. **Syntax** ```bash theme={null} circle transaction accelerate --address --chain ``` **Arguments** | Argument | Description | | :------- | :-------------- | | `` | Transaction ID. | **Options** | Option | Description | | :---------- | :-------------- | | `--address` | Wallet address. | | `--chain` | Blockchain. | **Example** ```bash theme={null} circle transaction accelerate abc-123 --address 0x... --chain ARC-TESTNET ``` *** ## Contract commands Interact with onchain contracts. ### `circle contract address` Show Circle contract addresses, optionally filtered by category and blockchain. **Syntax** ```bash theme={null} circle contract address [category] [--chain ] ``` **Arguments** | Argument | Description | | :----------- | :----------------------------------------------------------------------- | | `[category]` | Contract category to filter by (for example, `usdc`, `cctp`, `gateway`). | **Options** | Option | Description | | :-------- | :-------------------- | | `--chain` | Filter by blockchain. | **Examples** ```bash theme={null} circle contract address usdc --chain ARC-TESTNET circle contract address cctp --output json ``` *** ### `circle contract query` Execute a read-only contract call. **Syntax** ```bash theme={null} circle contract query [abiParameters...] --contract
--chain ``` **Arguments** | Argument | Description | | :----------------------- | :---------------------------------------------------------- | | `` | ABI function signature (for example, `balanceOf(address)`). | | `[abiParameters...]` | ABI parameters, space-separated (for example, `0x1234...`). | **Options** | Option | Description | | :----------- | :------------------- | | `--contract` | Contract address. | | `--chain` | Blockchain to query. | **Examples** ```bash theme={null} circle contract query "balanceOf(address)" 0xWALLET --contract 0xUSDC --chain ARC-TESTNET circle contract query "totalSupply()" --contract 0xUSDC --chain ARC-TESTNET --output json ``` *** ## Skill commands Discover and install skills from the [`circlefin/skills`](https://github.com/circlefin/skills) catalog. The `--tool` option specifies your agent framework. Common values: `claude-code`, `cursor`, `codex`. ### `circle skill list` List all available skills from the catalog. **Syntax** ```bash theme={null} circle skill list [--output json] ``` **Options** | Option | Description | | :-------------- | :---------------------- | | `--output json` | Return results as JSON. | *** ### `circle skill info` Show details and full content for a specific skill. **Syntax** ```bash theme={null} circle skill info --name ``` **Options** | Option | Description | | :------- | :---------------------------- | | `--name` | Name of the skill to inspect. | **Example** ```bash theme={null} circle skill info --name ``` *** ### `circle skill install` Install a skill into your agent framework. **Syntax** ```bash theme={null} circle skill install --tool [--name ] ``` **Options** | Option | Description | | :------- | :------------------------------------------------------------------------------------------ | | `--tool` | Agent framework to install into. Use multiple `--tool` options for more than one framework. | | `--name` | Skill name to install. Omit to install all available skills. | **Examples** ```bash theme={null} circle skill install --tool claude-code --name circle skill install --tool cursor --tool codex --name ``` *** ### `circle skill update` Update installed skills for an agent framework. **Syntax** ```bash theme={null} circle skill update --tool ``` **Options** | Option | Description | | :------- | :------------------------------------ | | `--tool` | Agent framework to update skills for. | *** ## Terms commands Inspect, accept, or reset your local Circle CLI Terms of Use acceptance record. The first time you run any command, Circle CLI prompts you to accept the Terms of Use and Privacy Policy. Acceptance is stored locally and reused on subsequent runs. To handle Terms acceptance non-interactively in scripts and AI agents, use `circle terms accept` or set `CIRCLE_ACCEPT_TERMS=1` in the environment. ### `circle terms` Show the current acceptance status and the canonical Terms of Use and Privacy Policy URLs. Default verb is `show`. **Syntax** ```bash theme={null} circle terms [show] [options] ``` **Options** | Option | Description | | :------- | :----------------------------------------------------------------------------------------------------------------------- | | `--init` | Return Terms info (version, URLs, notice text) for an agent to present before calling `accept`. Implies `--output json`. | **Examples** ```bash theme={null} circle terms circle terms --output json circle terms show --init --output json ``` **JSON output** ```json theme={null} { "accepted": true, "currentVersion": "1.0.0", "termsOfUseUrl": "https://www.circle.com/legal/circle-cli-terms-of-use", "privacyPolicyUrl": "https://www.circle.com/legal/privacy-policy", "acceptance": { "version": "1.0.0", "acceptedAt": "2026-05-07T12:34:56Z" } } ``` *** ### `circle terms accept` Explicitly accept the Terms of Use and Privacy Policy. Use this in scripts and AI agent workflows after the user provides explicit consent. **Syntax** ```bash theme={null} circle terms accept [--output json] ``` **Example** ```bash theme={null} circle terms accept --output json ``` *** ### `circle terms reset` Clear the local acceptance record. The next command run prompts you to accept the Terms again. **Syntax** ```bash theme={null} circle terms reset ``` *** ## Telemetry commands Manage CLI telemetry preferences. Telemetry collects privacy-preserving usage data to help improve Circle CLI. ### `circle telemetry status` Show the current telemetry preference. **Syntax** ```bash theme={null} circle telemetry status [options] ``` **Example** ```bash theme={null} circle telemetry status ``` *** ### `circle telemetry enable` Enable telemetry for future commands. **Syntax** ```bash theme={null} circle telemetry enable ``` *** ### `circle telemetry disable` Disable telemetry for future commands. **Syntax** ```bash theme={null} circle telemetry disable ``` # Start building with AI Source: https://developers.circle.com/ai/chatbot Collaborate with AI to generate code or learn how to build with Circle. You can generate code for any of these Circle products: Circle Wallets, Contracts, CCTP, Gateway. For AI-assisted development in your IDE, you can also use [Circle's MCP Server](/ai/mcp) or install [AI skills](/ai/skills) for specialized guidance. # Use Circle's MCP Server in Your IDE Source: https://developers.circle.com/ai/mcp Integrate Circle's MCP server to let your LLM or AI-assisted IDE generate and fix code for crypto apps using Circle's offerings, including Wallets, Contracts, CCTP, and Gateway. CLI commands ```shell Claude Code icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/claude-logo.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=2711bd5dc795308c62570a011248ef2d" theme={null} claude mcp add --transport http circle https://api.circle.com/v1/codegen/mcp --scope user ``` ```shell Codex icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/open-ai-logo-1.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=6a603cbffbbef93cfa078c899e8f19e8" theme={null} codex mcp add circle --url https://api.circle.com/v1/codegen/mcp ``` ## Quick setup Add Circle's MCP server to your client with the following details: * **Server Name**: `circle` * **Server URL**: `https://api.circle.com/v1/codegen/mcp` ## Installation with your IDE Select your MCP client to view detailed setup instructions: > **Note:** If you are using a client that is not listed here, you can still use > the Circle MCP server by manually adding the server URL to your client's > configuration. Download and install [Cursor](https://cursor.com) if you haven't already. [1-Click Set Up](cursor://anysphere.cursor-deeplink/mcp/install?name=circle\&config=eyJ1cmwiOiJodHRwczovL2FwaS5jaXJjbGUuY29tL3YxL2NvZGVnZW4vbWNwIn0%3D) Manual steps: 1. Open a project in Cursor and navigate to **Cursor Settings**. 2. In the settings menu, go to the **MCP** section. 3. Click **New MCP Server**. This will open your `mcp.json` configuration file. 4. Add the following configuration: ```json theme={null} { "mcpServers": { "circle": { "url": "https://api.circle.com/v1/codegen/mcp" } } } ``` 5. Return to the MCP settings page and enable the server using the toggle switch next to `circle`. Start generating code for Circle Wallets, Contracts, CCTP, and Gateway. For more information on how to use MCP with Cursor, see the [Cursor MCP documentation](https://cursor.com/docs/context/mcp). Download and install [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) if you haven't already. 1. Using the Claude Code command line add the MCP server with the following command: ```shell theme={null} claude mcp add --transport http circle https://api.circle.com/v1/codegen/mcp --scope user ``` 2. Verify the server is added by running the following command: ```shell theme={null} claude mcp get circle ``` 3. Start generating code for Circle Wallets, Contracts, CCTP, and Gateway. For more information on how to use MCP with Claude Code, see the [Claude Code MCP documentation](https://docs.claude.com/en/docs/claude-code/mcp). Download and install [Windsurf](https://docs.windsurf.com/windsurf/getting-started) if you haven't already. 1. Open the `~/.codeium/windsurf/mcp_config.json` file and add the following: ```json theme={null} { "mcpServers": { "circle": { "url": "https://api.circle.com/v1/codegen/mcp" } } } ``` 2. Enable the Circle MCP server in your MCP settings. Start generating code for Circle Wallets, Contracts, CCTP, and Gateway. For more information on how to use MCP with Windsurf, see the [Windsurf MCP documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Download and install [Kiro](https://kiro.dev) if you haven't already. 1. Open Kiro and go to **Preferences/Settings** → search for "MCP" → enable MCP support. 2. Create or open the MCP config file: * **Workspace-level**: `./.kiro/settings/mcp.json` (recommended for project-specific) * **User-level**: `~/.kiro/settings/mcp.json` 3. Add a server configuration entry for Circle: ```json theme={null} { "mcpServers": { "circle": { "command": "npx", "args": ["-y", "@circle/mcp-server"], "env": { "CIRCLE_BASE_URL": "https://api.circle.com/v1/codegen/mcp" }, "disabled": false } } } ``` 4. Save and then restart Kiro (or reload the MCP server list) so the new server appears in the MCP tab. 5. In Kiro's side panel → **MCP Servers** tab → you should see "circle-mcp" listed. Start generating code for Circle Wallets, Contracts, CCTP, and Gateway. Kiro's general MCP setup: [Kiro Docs - MCP](https://kiro.dev/docs/mcp/). ``` ``` # AI Skills for Building with Circle Source: https://developers.circle.com/ai/skills Use Circle's open source AI skills to accelerate development with AI-assisted IDEs. Skills provide specialized knowledge for building with Circle's products, including wallets, crosschain transfers, and smart contracts. Skills are available in the [circlefin/skills](https://github.com/circlefin/skills) repository. ## Installation Use the following commands to install Circle Skills with the command-line. ```shell Claude Code icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/claude-logo.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=2711bd5dc795308c62570a011248ef2d" theme={null} /plugin marketplace add circlefin/skills /plugin install circle-skills@circle ``` ```shell Vercel Skills CLI icon="https://mintcdn.com/circle-167b8d39/kF52uen9TY9sspam/images/vercel_logo.png?fit=max&auto=format&n=kF52uen9TY9sspam&q=85&s=20d2a3e59acc98f14fe41854458cfcd5" theme={null} npx skills add circlefin/skills ``` ## Available skills The following skills are available to help you build with Circle's products. ### `bridge-stablecoin` Build apps that bridge USDC between chains using Circle's [Cross-Chain Transfer Protocol (CCTP)](/cctp). Includes UX patterns, progress tracking, destination chain linking, and [Bridge Kit](https://docs.arc.io/app-kit/bridge) SDK implementation patterns for EVM and Solana chains. ### `use-arc` Build on Arc, Circle's blockchain where USDC is the native gas token. Covers chain configuration, [smart contract](/contracts) deployment with Foundry or Hardhat, frontend integration with viem/wagmi, and bridging USDC to Arc via [CCTP](/cctp). ### `use-circle-wallets` Choose the right [Circle wallet](/wallets) type for your application. Compares [developer-controlled](/wallets/dev-controlled), [user-controlled](/wallets/user-controlled), and [modular](/wallets/modular) (passkey) wallets across custody model, key management, account types, and blockchain support. ### `use-developer-controlled-wallets` [Developer-controlled wallets](/wallets/dev-controlled) where developers manage wallet creation, storage, and key management. Use for custodial or operational flows like payouts, treasury movements, subscriptions, and automation. ### `use-gateway` Implement [Circle Gateway](/gateway) unified balance for crosschain USDC transfers. Supports instant transfers (under 500ms) across EVM and Solana chains with deposit, balance query, and transfer workflows. ### `use-modular-wallets` Build [modular wallets](/wallets/modular) with passkey authentication, gasless transactions, and modular architecture. Supports ERC-4337 account abstraction and ERC-6900 modular framework. ### `use-smart-contract-platform` Deploy, import, interact with, and monitor smart contracts using [Circle's Smart Contract Platform](/contracts). Supports bytecode deployment, template contracts (ERC-20/721/1155), ABI-based read/write calls, and event monitoring. ### `use-user-controlled-wallets` Build embedded [user-controlled wallets](/wallets/user-controlled) where users control their own assets. Supports Web2-like login experiences (Google, Facebook, Apple, email OTP, PIN) without seed phrases. # API Reference Source: https://developers.circle.com/api-reference Overview of Circle's available APIs and endpoints. Circle provides a suite of REST APIs for building financial applications on blockchain infrastructure. Whether you're creating wallets, deploying smart contracts, moving USDC across blockchains, or building institutional payment flows, there is an API tailored to your use case. ## Before you begin Many Circle APIs require an API key to authenticate requests. Permissionless products like CCTP and Gateway are open and require no API key. Learn about API keys, client keys, and kit keys, and how to authenticate requests to Circle's platform Use idempotency keys to safely retry API calls without creating duplicate operations ## Available APIs Create and manage developer-controlled and user-controlled wallets, execute transactions, and sign messages across EVM, Solana, and other supported blockchains Deploy and interact with smart contracts using Circle's managed infrastructure, including event monitoring and contract templates Fetch attestations and support native USDC transfers across blockchains using Cross-Chain Transfer Protocol Access and manage a unified USDC balance across multiple blockchains with instant transfers in under 500 ms Manage USDC and EURC balances, process crypto deposits and payouts, execute cross-currency trades, and manage reserves Route and settle stablecoin payments across Circle's network with support for quotes, payments, and transactions Request quotes and execute institutional FX trades between USDC and EURC with onchain settlement on Arc Deposit USDC into xReserve, retrieve attestations, and manage withdrawals for USDC-backed stablecoins # Get an attestation Source: https://developers.circle.com/api-reference/cctp/all/get-attestation /openapi/cctp.yaml get /v1/attestations/{messageHash} Retrieves the signed attestation for a USDC burn event on the source chain. # Get USDC transfer fees Source: https://developers.circle.com/api-reference/cctp/all/get-burn-usdc-fees /openapi/cctp.yaml get /v2/burn/USDC/fees/{sourceDomainId}/{destDomainId} Retrieves the applicable fees for a USDC transfer between the specified source and destination domains. The fee is returned in basis points (1 = 0.01%). # Get USDC Fast Transfer allowance Source: https://developers.circle.com/api-reference/cctp/all/get-fast-burn-usdc-allowance /openapi/cctp.yaml get /v2/fastBurn/USDC/allowance Retrieves the available USDC Fast Transfer allowance remaining. # Get a list of messages Source: https://developers.circle.com/api-reference/cctp/all/get-messages /openapi/cctp.yaml get /v1/messages/{sourceDomainId}/{transactionHash} Retrieves message and attestation details for CCTP V1 messages. # Get messages and attestations Source: https://developers.circle.com/api-reference/cctp/all/get-messages-v2 /openapi/cctp.yaml get /v2/messages/{sourceDomainId} Retrieves messages and attestations for a given transaction hash or nonce. Each message for a given transaction hash is ordered by ascending log index. # List attestation public keys Source: https://developers.circle.com/api-reference/cctp/all/get-public-keys /openapi/cctp.yaml get /v1/publicKeys Retrieves a list of the currently active public keys for verifying attestation signatures. # Get public keys Source: https://developers.circle.com/api-reference/cctp/all/get-public-keys-v2 /openapi/cctp.yaml get /v2/publicKeys Returns the public keys for validating attestations across all supported versions of CCTP. # Re-attest a pre-finality message Source: https://developers.circle.com/api-reference/cctp/all/reattest-message /openapi/cctp.yaml post /v2/reattest/{nonce} The re-attestation flow allows the relayer to obtain a higher level of finality than was originally requested on the source chain, while still being forced to pay the fee since allowance was reserved. This flow resolves the case where a sender specifies a finality threshold lower than the destination chain recipient requires. # Create a CUBIX bank account Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-cubix-account /openapi/account.yaml post /v1/businessAccount/banks/cubix # Create a deposit address Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-deposit-address /openapi/account.yaml post /v1/businessAccount/wallets/addresses/deposit Generates a new blockchain address for a wallet for a given currency/chain pair. Circle may reuse addresses on blockchains that support reuse. For example, if you're requesting two addresses for depositing USD and ETH, both on Ethereum, you may see the same Ethereum address returned. Depositing cryptocurrency to a generated address will credit the associated wallet with the value of the deposit. Note: Circle Mint Singapore customers must verify all transfer recipients using the UI in the Circle Console, as transfers from unverified addresses will be held in `pending` status. # Create a payout Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-payout /openapi/account.yaml post /v1/businessAccount/payouts Create a redemption (offramp) payout. This payout converts a digital asset to fiat currency. # Create a PIX bank account Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-pix-account /openapi/account.yaml post /v1/businessAccount/banks/pix # Create a recipient address Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-recipient-address /openapi/account.yaml post /v1/businessAccount/wallets/addresses/recipient Stores an external blockchain address. Once added, the recipient address must be verified to ensure that you know and trust each new address. **For France customers:** Circle Mint France customers must verify all transfer recipients using the UI in the Circle Console, as transfers from unverified addresses will be held in pending status. Please see Help Center articles below for details: - [Circle Mint France Travel Rule](https://help.circle.com/s/article/Circle-Mint-France-Travel-Rule) - [Circle Mint France wallet verification](https://help.circle.com/s/article/Circle-Mint-France-wallet-verification) # Create a transfer Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-transfer /openapi/account.yaml post /v1/businessAccount/transfers A transfer can be made from an existing business account to a blockchain location. # Create a wire bank account Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-wire-account /openapi/account.yaml post /v1/businessAccount/banks/wires # Create a mock Wire payment Source: https://developers.circle.com/api-reference/circle-mint/account/create-mock-wire-payment /openapi/account.yaml post /v1/mocks/payments/wire In the sandbox environment, initiate a mock wire payment that mimics the behavior of funds sent through the bank (wire) account linked to master wallet. # Delete a recipient address Source: https://developers.circle.com/api-reference/circle-mint/account/delete-business-recipient-address /openapi/account.yaml delete /v1/businessAccount/wallets/addresses/recipient/{id} Deletes an external blockchain address. The recipient address must be in an 'active' or 'pending' state in order to be deleted successfully. # Get associated accounts Source: https://developers.circle.com/api-reference/circle-mint/account/get-associated-accounts /openapi/account.yaml get /v1/businessAccount/associatedAccounts Returns a list of sibling CMAs that are associated with the current account under unified credentials. These accounts can be used as destinations for cross-entity transfers. # Get a CUBIX bank account Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-cubix-account /openapi/account.yaml get /v1/businessAccount/banks/cubix/{id} # Get CUBIX instructions Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-cubix-account-instructions /openapi/account.yaml get /v1/businessAccount/banks/cubix/{id}/instructions Get the CUBIX transfer instructions into the Circle bank account given your fiat account id. # List all deposit addresses Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-deposit-address /openapi/account.yaml get /v1/businessAccount/wallets/addresses/deposit Returns a list of deposit addresses for a given wallet. # Get a deposit by ID Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-deposit-by-id /openapi/account.yaml get /v1/businessAccount/deposits/{id} Returns a deposit by ID. # Get a payout Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-payout /openapi/account.yaml get /v1/businessAccount/payouts/{id} # Get a PIX bank account Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-pix-account /openapi/account.yaml get /v1/businessAccount/banks/pix/{id} # Get PIX instructions Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-pix-account-instructions /openapi/account.yaml get /v1/businessAccount/banks/pix/{id}/instructions Get the PIX transfer instructions into the Circle bank account given your bank account id. # Get a transfer Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-transfer /openapi/account.yaml get /v1/businessAccount/transfers/{id} # Get a wire bank account Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-wire-account /openapi/account.yaml get /v1/businessAccount/banks/wires/{id} # Get wire instructions Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-wire-account-instructions /openapi/account.yaml get /v1/businessAccount/banks/wires/{id}/instructions Get the wire transfer instructions into the Circle bank account given your bank account ID. # Get PIX routing info Source: https://developers.circle.com/api-reference/circle-mint/account/get-pix-routing-info /openapi/account.yaml get /v1/businessAccount/banks/pix/{fiatAccountId}/routingInfo Retrieves available settlement banks and current routing configuration for a PIX fiat account. # Get report by ID Source: https://developers.circle.com/api-reference/circle-mint/account/get-report-by-id /openapi/account.yaml get /v1/reports/{id} Returns the current metadata for a report, including a fresh pre-signed `downloadUrl` when the report is `ready`. Use this endpoint to check the status of a `pending` report or to obtain a new download URL after the previous one has expired. # Download report content Source: https://developers.circle.com/api-reference/circle-mint/account/get-report-content /openapi/account.yaml get /v1/reports/{id}/content Streams the raw report content as a file download, an alternative to following `downloadUrl` from the JSON response. Returns `409` if the report is not yet `ready`. # Get wire routing info Source: https://developers.circle.com/api-reference/circle-mint/account/get-wire-routing-info /openapi/account.yaml get /v1/businessAccount/banks/wires/{fiatAccountId}/routingInfo Retrieves available settlement banks and current routing configuration for a wire fiat account. # List all balances Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-balances /openapi/account.yaml get /v1/businessAccount/balances Retrieves the balance of funds that are available for use. # List all CUBIX bank accounts. Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-cubix-accounts /openapi/account.yaml get /v1/businessAccount/banks/cubix # List all deposits Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-deposits /openapi/account.yaml get /v1/businessAccount/deposits Searches for deposits sent to your business account. If the date parameters are omitted, returns the most recent deposits. This endpoint returns up to 50 deposits in descending chronological order or pageSize, if provided. # List all payouts Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-payouts /openapi/account.yaml get /v1/businessAccount/payouts Lists all payouts for your account. Note that this endpoint does not return the tracking reference number for the payouts in the response. If you need that information you must get each payout individually by ID. # List all PIX bank accounts. Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-pix-accounts /openapi/account.yaml get /v1/businessAccount/banks/pix # List all recipient addresses Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-recipient-addresses /openapi/account.yaml get /v1/businessAccount/wallets/addresses/recipient Returns a list of recipient addresses that have each been verified and are eligible for transfers. Any recipient addresses pending administrator verification are not included in the response. # List all transfers Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-transfers /openapi/account.yaml get /v1/businessAccount/transfers Searches for transfers from your business account. If the date parameters are omitted, returns the most recent transfers. This endpoint returns up to 50 transfers in descending chronological order or pageSize, if provided. # List all wire bank accounts Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-wire-accounts /openapi/account.yaml get /v1/businessAccount/banks/wires # List daily burn fee calculations Source: https://developers.circle.com/api-reference/circle-mint/account/list-net-burn-fee-daily-calculations /openapi/account.yaml get /v1/fees/redemption/dailyReports Returns daily burn fee calculations. This endpoint returns up to 50 daily fee calculations in descending chronological order or `pageSize`, if provided. # Request a report Source: https://developers.circle.com/api-reference/circle-mint/account/request-report /openapi/account.yaml post /v1/reports Submits a report generation request. `reportType` specifies the type of report and required fields. If ready, returns `200` with a pre-signed `downloadUrl`. If not, returns `202` with status `pending`. Poll `GET /v1/reports/{id}` for status, or download using `GET /v1/reports/{id}/content` when ready. Requests are idempotent: the same entity and request parameters always produce the same report ID. Retries return the existing report. # Update PIX routing preferences Source: https://developers.circle.com/api-reference/circle-mint/account/update-pix-routing-preferences /openapi/account.yaml put /v1/businessAccount/banks/pix/{fiatAccountId}/routingPreferences Creates or updates the settlement bank routing preferences for a PIX fiat account. At least one of `inboundBankLabel` or `outboundBankLabel` must be provided in the request. Note: This endpoint will reject updates if the account has active Express routes configured. Accounts with Express routes must update preferences through the Circle Mint UI. # Update wire routing preferences Source: https://developers.circle.com/api-reference/circle-mint/account/update-wire-routing-preferences /openapi/account.yaml put /v1/businessAccount/banks/wires/{fiatAccountId}/routingPreferences Creates or updates the settlement bank routing preferences for a wire fiat account. At least one of `inboundBankLabel` or `outboundBankLabel` must be provided in the request. Note: This endpoint will reject updates if the account has active Express routes configured. Accounts with Express routes must update preferences through the Circle Mint UI. # Cancel reserved funds Source: https://developers.circle.com/api-reference/circle-mint/credit/cancel-credit-transfer-reserve /openapi/credit.yaml put /v1/credit/transfers/{id}/cancelReserve Cancels a `funds_reserved` transfer and releases the reserved amount back to available credit. The transfer must be in `funds_reserved` status for this operation. **Note:** This endpoint is only available for Settlement Advance products. # Initiate a crypto repayment Source: https://developers.circle.com/api-reference/circle-mint/credit/create-credit-crypto-repayment /openapi/credit.yaml post /v1/credit/cryptoRepayment Initiates a crypto repayment for a credit transfer. The requested amount is capped at the outstanding balance. If the minimum of the requested amount and outstanding balance is zero, the request will be rejected with HTTP 400. **Note:** This endpoint is only available for Line of Credit products. Crypto repayment is not supported for Settlement Advance products. # Create a credit transfer Source: https://developers.circle.com/api-reference/circle-mint/credit/create-credit-transfer /openapi/credit.yaml post /v1/credit/transfers Requests a new credit transfer (drawdown) from the credit line. Disbursement is asynchronous. **Note:** This endpoint is only available for Line of Credit products. Settlement Advance products must use the reserve funds flow. # Create a mock wire repayment Source: https://developers.circle.com/api-reference/circle-mint/credit/create-mock-wire-repayment /openapi/credit.yaml post /v1/credit/mocks/repayments In the sandbox environment, initiate a mock wire repayment that simulates an incoming wire payment to repay an outstanding credit transfer. The fiat account will be automatically linked as a repayment account if not already linked. # Get a credit fee Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-fee /openapi/credit.yaml get /v1/credit/fees/{id} Returns detailed information about a specific fee. # Get credit line details Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-line /openapi/credit.yaml get /v1/credit Provides overall credit line details, including status, available limit, and outstanding balance. # Get a credit repayment Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-repayment /openapi/credit.yaml get /v1/credit/repayments/{id} Returns detailed information about a specific repayment. # Get a credit transfer Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-transfer /openapi/credit.yaml get /v1/credit/transfers/{id} Returns detailed information about a specific credit transfer. Fields `outstanding`, `fees`, `dueDate`, and `disbursedDate` are only present once the transfer reaches `disbursed`, `paid`, or `past_due` status. # Get repayment account details Source: https://developers.circle.com/api-reference/circle-mint/credit/get-repayment-account-detail /openapi/credit.yaml get /v1/credit/repaymentAccounts/{fiatAccountId} Returns repayment account details and wire instructions for making a credit repayment using the specified fiat account. # List all credit fees Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-fees /openapi/credit.yaml get /v1/credit/fees Returns a paginated list of all historical fees. Filterable by create date range, currency, and status. # List all credit repayments Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-repayments /openapi/credit.yaml get /v1/credit/repayments Returns a paginated list of all repayments (fiat and crypto). Filterable by create date range, transfer ID, type, and status. # List all credit transfers Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-transfers /openapi/credit.yaml get /v1/credit/transfers Returns a paginated list of all credit transfers. Filterable by create date range and status. Transfer items in list responses do not include outstanding and fees properties. # Request reserved funds Source: https://developers.circle.com/api-reference/circle-mint/credit/request-credit-transfer-reserved-funds /openapi/credit.yaml put /v1/credit/transfers/{id}/requestReservedFunds Transitions a `funds_reserved` transfer to `requested` status by uploading evidence (wire proof). This initiates the manual approval process for Settlement Advance transfers. The request must include an evidence file (wire proof) as multipart form data. Allowed file types are `application/pdf`, `image/jpeg`, and `image/png`. **Note:** This endpoint is only available for Settlement Advance products. # Reserve funds for a credit transfer Source: https://developers.circle.com/api-reference/circle-mint/credit/reserve-credit-transfer-funds /openapi/credit.yaml post /v1/credit/transfers/reserveFunds Reserves funds for a Settlement Advance draw. This is the first mandatory step in the Settlement Advance transfer flow. Reserved funds expire after 30 minutes if not progressed to `requested` status via the request reserved funds endpoint. Only one transfer may be in `funds_reserved` status per credit line at a time. **Note:** This endpoint is only available for Settlement Advance products. # Create FX account Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-fx-account /openapi/cross-currency.yaml put /v1/exchange/fxConfigs/accounts Creates a currency trading account # Create FX trade Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-fx-trade /openapi/cross-currency.yaml post /v1/exchange/trades Creates a cross-currency trade # Create a mock PIX payment Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-mock-pix-payment /openapi/cross-currency.yaml post /v1/mocks/payments/pix Initiates a mock PIX payment in the sandbox environment that mimics the behavior of funds sent through the bank account linked to the main wallet. # Get daily currency exchange limits Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-daily-fx-limits /openapi/cross-currency.yaml get /v1/exchange/fxConfigs/dailyLimits Returns daily currency exchange limits and usages. # Get FX trade Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-fx-trade-id /openapi/cross-currency.yaml get /v1/exchange/trades/{id} Returns an FX trade by ID. # Get all FX trades Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-fx-trades /openapi/cross-currency.yaml get /v1/exchange/trades Returns all cross-currency trades. You can include an optional `settlementId` query parameter to filter the trades to only a specific settlement. # Get quote Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-quote /openapi/cross-currency.yaml post /v1/exchange/quotes Fetches an indicative exchange rate between two currencies. Either the from currency or to currency must be USD. Note: The current market exchange rate will be applied when Circle receives the deposit. # Get settlement Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlement-id /openapi/cross-currency.yaml get /v1/exchange/trades/settlements/{id} Returns a settlement by ID. # Get settlement instructions Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlement-instructions /openapi/cross-currency.yaml get /v1/exchange/trades/settlements/instructions/{currency} Returns settlement instructions for a specific currency. # Get all settlements Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlements /openapi/cross-currency.yaml get /v1/exchange/trades/settlements Returns all settlements. # Create a notification subscription Source: https://developers.circle.com/api-reference/circle-mint/general/create-subscription /openapi/general.yaml post /v1/notifications/subscriptions Subscribe to receiving notifications at a given endpoint. The endpoint should be able to handle AWS SNS subscription requests. For more details see https://docs.aws.amazon.com/mobile/sdkforxamarin/developerguide/sns-send-http.html. Note, the sandbox environment allows a maximum of 3 active subscriptions; otherwise, this is limited to 1 active subscription and subsequent create requests will be rejected with a Limit Exceeded error. # Remove a notification subscription Source: https://developers.circle.com/api-reference/circle-mint/general/delete-subscription /openapi/general.yaml delete /v1/notifications/subscriptions/{id} To remove a subscription, all its subscription requests' statuses must be either 'confirmed', 'deleted' or a combination of those. A subscription with at least one 'pending' subscription request cannot be removed. # Get configuration info Source: https://developers.circle.com/api-reference/circle-mint/general/get-account-config /openapi/general.yaml get /v1/configuration Retrieves general configuration information. # List all stablecoins Source: https://developers.circle.com/api-reference/circle-mint/general/list-stablecoins /openapi/general.yaml get /v1/stablecoins Retrieves total circulating supply for supported stablecoins across all chains. This endpoint is rate limited to one call per minute (based on IP). # List all notification subscriptions Source: https://developers.circle.com/api-reference/circle-mint/general/list-subscriptions /openapi/general.yaml get /v1/notifications/subscriptions Retrieve a list of existing notification subscriptions with details. # Ping Source: https://developers.circle.com/api-reference/circle-mint/general/ping /openapi/general.yaml get /ping Checks that the service is running. # Create an external entity Source: https://developers.circle.com/api-reference/circle-mint/institutional/create-external-entity /openapi/institutional.yaml post /v1/externalEntities Creates an external entity for the institutional account. To access the Core API for Institutions, contact your Circle account representative. # Get all external entities Source: https://developers.circle.com/api-reference/circle-mint/institutional/get-all-external-entities /openapi/institutional.yaml get /v1/externalEntities Returns all external entities for the institutional account. To access the Core API for Institutions, contact your Circle account representative. Note that the `businessUniqueIdentifier` and `identifierIssuingCountryCode` must both be provided, or not at all. Only providing one will result in an error. # Get an external entity by wallet ID Source: https://developers.circle.com/api-reference/circle-mint/institutional/get-external-entity-by-wallet-id /openapi/institutional.yaml get /v1/externalEntities/{walletId} Returns an external entity by wallet ID. To access the Core API for Institutions, contact your Circle account representative. # Create a payment intent Source: https://developers.circle.com/api-reference/circle-mint/payments/create-payment-intent /openapi/payments.yaml post /v1/paymentIntents Create a continuous (default) or transient payment intent. Continuous payment intents are created by default. To create a transient payment intent, the type field must be explicitly set to 'transient'. # Expire a payment intent Source: https://developers.circle.com/api-reference/circle-mint/payments/expire-payment-intent /openapi/payments.yaml post /v1/paymentIntents/{id}/expire # Get a payment Source: https://developers.circle.com/api-reference/circle-mint/payments/get-payment /openapi/payments.yaml get /v1/payments/{id} # Get a payment intent Source: https://developers.circle.com/api-reference/circle-mint/payments/get-payment-intent /openapi/payments.yaml get /v1/paymentIntents/{id} # List all payment intents Source: https://developers.circle.com/api-reference/circle-mint/payments/list-payment-intents /openapi/payments.yaml get /v1/paymentIntents # List all payments Source: https://developers.circle.com/api-reference/circle-mint/payments/list-payments /openapi/payments.yaml get /v1/payments # Refund a payment intent Source: https://developers.circle.com/api-reference/circle-mint/payments/refund-payment-intent /openapi/payments.yaml post /v1/paymentIntents/{id}/refund # Create a recipient Source: https://developers.circle.com/api-reference/circle-mint/payouts/create-address-book-recipient /openapi/payouts.yaml post /v1/addressBook/recipients Creates an address book recipient. Required fields depend on your Circle entity; use the request body options that match your integration. Validation failures can return error codes in the `2024`–`2037` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing). # Create a payout Source: https://developers.circle.com/api-reference/circle-mint/payouts/create-payout /openapi/payouts.yaml post /v1/payouts Create a stablecoin payout. The following table includes the supported pairs of `amount.currency` and `toAmount.currency` for stablecoin address book payouts: | amount.currency | toAmount.currency | | ---------------- | ----------------- | | USD | USD | | EUR | EUR | Required fields depend on your Circle entity; use the request body options that match your integration. For Singapore (CIRCLE_SG) entities, `purposeOfTransfer` is **required** and must use a payment reason code from [Crypto Payouts payment reason codes](https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes). Invalid or missing values can return error code `5020`. # Delete a recipient Source: https://developers.circle.com/api-reference/circle-mint/payouts/delete-address-book-recipient /openapi/payouts.yaml delete /v1/addressBook/recipients/{id} # Get a recipient Source: https://developers.circle.com/api-reference/circle-mint/payouts/get-address-book-recipient /openapi/payouts.yaml get /v1/addressBook/recipients/{id} # Get a payout Source: https://developers.circle.com/api-reference/circle-mint/payouts/get-payout /openapi/payouts.yaml get /v1/payouts/{id} # List all recipients Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-address-book-recipients /openapi/payouts.yaml get /v1/addressBook/recipients # List VASPs Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-address-book-vasps /openapi/payouts.yaml get /v1/addressBook/vasps Returns active Virtual Asset Service Providers (VASPs) available for the customer's jurisdiction. Use returned `id` values as `vaspId` in `ownership.custody` when creating a recipient and custody is `hosted`. **Note:** This operation is supported only for Circle Singapore (SG) customers. # List all payouts Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-payouts /openapi/payouts.yaml get /v1/payouts # Modify a recipient Source: https://developers.circle.com/api-reference/circle-mint/payouts/modify-address-book-recipient /openapi/payouts.yaml patch /v1/addressBook/recipients/{id} Updates address book recipient metadata. # Create daily custody balance report Source: https://developers.circle.com/api-reference/circle-mint/reserve-management/report-daily-custody-balances /openapi/reserve-management.yaml post /v2/reserveManagement/dailyCustodyBalances Creates a daily custody balance report for USDC and EURC and sends it to Circle. For `reportType=eea`, this endpoint represents the four reportable cells of EBA Template S 08.00 for one token and one reference date: CASP total token count and EUR value, plus the EU-client subset token count and EUR value. MiCA / EBA S 08.00 mapping (`reportType=eea`): - S 08.00 Row 0010 / Col 0010 maps to `additionalFields.totalBalance` - S 08.00 Row 0010 / Col 0020 maps to `additionalFields.equivalentEuroTotalBalance` - S 08.00 Row 0020 / Col 0010 maps to `localBalance` - S 08.00 Row 0020 / Col 0020 maps to `additionalFields.equivalentEuroLocalBalance` `localBalance` and `totalBalance` are token-unit counts. `equivalentEuroLocalBalance` and `equivalentEuroTotalBalance` are EUR values. EU clients are determined by habitual residence for natural persons and registered office for legal persons. USD/EUR FX conversion should be based on the ECB rate applicable for that date, as available on the ECB website. Validation rules: - `localBalance` must be less than or equal to `additionalFields.totalBalance` - `equivalentEuroLocalBalance` must be less than or equal to `equivalentEuroTotalBalance` - Only one submission per day per currency - USDC and EURC require separate submissions # Create a notification subscription Source: https://developers.circle.com/api-reference/contracts/common/create-subscription /openapi/configurations_2.yaml post /v2/notifications/subscriptions Create a notification subscription by configuring an endpoint to receive notifications. For details, see the [Notification Flows](https://developers.circle.com/wallets/webhook-notification-flows) guide. # Delete a notification subscription Source: https://developers.circle.com/api-reference/contracts/common/delete-subscription /openapi/configurations_2.yaml delete /v2/notifications/subscriptions/{id} Delete an existing subscription. # Get a notification signature public key Source: https://developers.circle.com/api-reference/contracts/common/get-notification-signature /openapi/configurations_2.yaml get /v2/notifications/publicKey/{id} Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle. In the headers of each webhook, you can find 1. `X-Circle-Signature`: a header containing the digital signature generated by Circle. 2. `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key. # Retrieve a notification subscription Source: https://developers.circle.com/api-reference/contracts/common/get-subscription /openapi/configurations_2.yaml get /v2/notifications/subscriptions/{id} Retrieve an existing notification subscription. # Get all notification subscriptions Source: https://developers.circle.com/api-reference/contracts/common/get-subscriptions /openapi/configurations_2.yaml get /v2/notifications/subscriptions Retrieve an array of existing notification subscriptions. # Ping Source: https://developers.circle.com/api-reference/contracts/common/ping /openapi/configurations_2.yaml get /ping Checks that the service is running. # Update a notification subscription Source: https://developers.circle.com/api-reference/contracts/common/update-subscription /openapi/configurations_2.yaml patch /v2/notifications/subscriptions/{id} Update subscription endpoint to receive notifications. # Create Event Monitor Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/create-event-monitor /openapi/smart-contract-platform.yaml post /v1/w3s/contracts/monitors Create a new event monitor based on the provided blockchain, contract address, and event signature. # Delete Event Monitor Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/delete-event-monitor /openapi/smart-contract-platform.yaml delete /v1/w3s/contracts/monitors/{id} Delete an existing event monitor given its ID. # Deploy a contract Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract /openapi/smart-contract-platform.yaml post /v1/w3s/contracts/deploy Deploy a smart contract on a specified blockchain using the contract's ABI and bytecode. The deployment will originate from one of your Circle Wallets. # Deploy a contract from a template Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template /openapi/smart-contract-platform.yaml post /v1/w3s/templates/{id}/deploy Deploy a smart contract using a template. # Estimate a contract deployment Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/estimate-contract-deploy /openapi/smart-contract-platform.yaml post /v1/w3s/contracts/deploy/estimateFee Estimate the network fee for deploying a smart contract on a specified blockchain, given the contract bytecode. # Estimate fee for a contract template deployment Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/estimate-contract-template-deploy /openapi/smart-contract-platform.yaml post /v1/w3s/templates/{id}/deploy/estimateFee Estimate the fee required to deploy contract by template. # Get a contract Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract /openapi/smart-contract-platform.yaml get /v1/w3s/contracts/{id} Get a single contract that you've imported or deployed. Retrieved using the contracts ID as opposed to the on-chain address. # Get Event Monitors Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-event-monitors /openapi/smart-contract-platform.yaml get /v1/w3s/contracts/monitors Fetch a list of event monitors, optionally filtered by blockchain, contract address, and event signature. # Import a contract Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/import-contract /openapi/smart-contract-platform.yaml post /v1/w3s/contracts/import Add an existing smart contract to your library of contracts. It also can be done in the Developer Services Console. # List contracts Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/list-contracts /openapi/smart-contract-platform.yaml get /v1/w3s/contracts Fetch a list of contracts that you've imported and/or deployed. # Get Event Logs Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/list-event-logs /openapi/smart-contract-platform.yaml get /v1/w3s/contracts/events Fetch all event logs, optionally filtered by blockchain and contract address. # Execute a query function on a contract Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/query-contract /openapi/smart-contract-platform.yaml post /v1/w3s/contracts/query Query the state of a contract by providing the address and blockchain. # Update a contract Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/update-contract /openapi/smart-contract-platform.yaml patch /v1/w3s/contracts/{id} Update the off-chain properties, such as description, of a contract that you've imported or deployed. Updated using the contracts ID as opposed to the on-chain address. # Update an Event Monitor Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/update-event-monitor /openapi/smart-contract-platform.yaml put /v1/w3s/contracts/monitors/{id} Update an existing event monitor given its ID. # Create a webhook subscription Source: https://developers.circle.com/api-reference/cpn/common/create-subscription /openapi/configurations.yaml post /v2/cpn/notifications/subscriptions Create a webhook subscription by configuring an endpoint to receive notifications. # Delete a notification subscription Source: https://developers.circle.com/api-reference/cpn/common/delete-subscription /openapi/configurations.yaml delete /v2/cpn/notifications/subscriptions/{id} Delete an existing subscription. # Get a notification signature public key Source: https://developers.circle.com/api-reference/cpn/common/get-notification-signature /openapi/configurations.yaml get /v2/cpn/notifications/publicKey/{id} Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle. In the headers of each webhook, you can find - `X-Circle-Signature`: a header containing the digital signature generated by Circle. - `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key. # Get a notification subscription Source: https://developers.circle.com/api-reference/cpn/common/get-subscription /openapi/configurations.yaml get /v2/cpn/notifications/subscriptions/{id} Returns an existing notification subscription. # Get all webhook subscriptions Source: https://developers.circle.com/api-reference/cpn/common/get-subscriptions /openapi/configurations.yaml get /v2/cpn/notifications/subscriptions Returns an array of existing webhook subscriptions. # Ping Source: https://developers.circle.com/api-reference/cpn/common/ping /openapi/configurations.yaml get /ping Checks that the service is running. # Update a notification subscription Source: https://developers.circle.com/api-reference/cpn/common/update-subscription /openapi/configurations.yaml patch /v2/cpn/notifications/subscriptions/{id} Update subscription endpoint to receive notifications. # Accelerate a stuck transaction Source: https://developers.circle.com/api-reference/cpn/cpn-platform/accelerate-transaction /openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions/accelerate - Accelerate a transaction based on the payment ID. It should be used when a transaction associated with the payment is broadcasted but not confirmed for a long period of time (i.e 10 minutes). This is usually due to gas fees being too low and not picked up by any miner/validator. - The /accelerate endpoint essentially creates another transaction with the same params as the broadcasted transaction. If multiple broadcasted transactions exist, it will use the newest created one. Afterwards, OFI can sign with a higher gas fee and submit via /submit endpoint to accelerate blockchain confirmation. - Requirements for using this endpoint: - No COMPLETED transaction exist for the payment (otherwise onchain transaction has completed) - No CREATED transaction exist for the payment, otherwise OFI should sign that transaction and submit - No PENDING transaction exist for the payment, otherwise OFI should wait for transaction to be broadcasted - In another word, all existing transaction for the payment should either be FAILED (which is no longer effective) or BROADCASTED (which means they are stuck onchain and not confirmed) # Create a payment Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-payment /openapi/cpn-ofi.yaml post /v1/cpn/payments Creates a payment by using the quote created previously and submitting recipient information (travel rule). The payment will remain valid if the onchain settlement occurs before settlementExpireDate. # Create a quote Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-quotes /openapi/cpn-ofi.yaml post /v1/cpn/quotes Creates one or more quotes for the given source/destination parameters. Returns quotes sorted in the following order: - Ascending of `sourceAmount` if your quote is based on `destinationAmount`. - Descending of `destinationAmount` if your quote is based on `sourceAmount`. # Create a support ticket Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-support-ticket /openapi/cpn-ofi.yaml post /v1/cpn/supportTickets Create transaction-related issues (for example, settlement delays, missing information, or refunds). These tickets are stored centrally in the CPN platform and routed to the appropriate party for resolution. # Create a transaction Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-transaction /openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions Creates an unsigned onchain transaction for a specific payment. # Create a transaction (V2) Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-transaction-v2 /openapi/cpn-ofi.yaml post /v2/cpn/payments/{paymentId}/transactions Create a V2 transaction for signing # Get a payment Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment /openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId} Returns the PII fields needed to collect to make this payment (i.e. travel rule and beneficiary account data) # Get payment configurations Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment-configurations-overview /openapi/cpn-ofi.yaml get /v1/cpn/configurations/overview Returns the overview of supported countries, currencies, payment methods, blockchains. # Get payment requirements for a quote Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment-requirements /openapi/cpn-ofi.yaml get /v1/cpn/payments/requirements Retrieves the PII fields needed to collect to make this payment (travel rule and beneficiary account data). # Get details of a quote Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-quote /openapi/cpn-ofi.yaml get /v1/cpn/quotes/{quoteId} Retrieve details of a specific quote (e.g., re-check expiration, fees). # Get refund details Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-refund /openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/refunds/{refundId} Retrieves the full refund object associated with a specific payment. This can be used by OFIs to reconcile refund status and verify refund completion. # Get details for an RFI Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-rfi /openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/rfis/{rfiId} Retrieve details of a specific RFI for a payment. If the BFI initiates an RFI after the payment is created, the OFI will be notified via webhook. This webhook will detail what specific information the OFI needs to send. After receiving the webhook. The OFI is expected to encrypt the requested data and send it to the BFI using CPN's RFI submit endpoint. Failure to respond to an RFI will result in a failed payment. The OFI will receive webhooks with the decision based on the submitted information. # Get a transaction Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-transaction /openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/transactions/{transactionId} Retrieves a specific transaction by its ID for a given payment # Get a transaction by ID (V2) Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-transaction-v2 /openapi/cpn-ofi.yaml get /v2/cpn/payments/{paymentId}/transactions/{transactionId} Get a transaction by ID # List payments Source: https://developers.circle.com/api-reference/cpn/cpn-platform/list-payments /openapi/cpn-ofi.yaml get /v1/cpn/payments Returns a list of all payments that fit the specified parameters. # Get supported payment routes Source: https://developers.circle.com/api-reference/cpn/cpn-platform/list-routes /openapi/cpn-ofi.yaml get /v1/cpn/configurations/routes Returns a list of route details including trade limits. This information can determine what corridors and parameters are valid for subsequent quote creation. # Submit RFI data Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-rfi /openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/rfis/{rfiId}/submit Submit encrypted RFI data to complete an RFI request from the BFI. # Submit a signed transaction for broadcast Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-transaction /openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions/{transactionId}/submit Return the signed hex string of the transaction, Circle will validate the content and broadcast to the chain. # Submit a signed transaction for broadcast (V2) Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-transaction-v2 /openapi/cpn-ofi.yaml post /v2/cpn/payments/{paymentId}/transactions/{transactionId}/submit Submit a signed V2 transaction for broadcast # Upload RFI file Source: https://developers.circle.com/api-reference/cpn/cpn-platform/upload-rfi-file /openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/rfis/{rfiId}/files Upload encrypted RFI file. # Create an account Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/create-account /openapi/accounts.yaml post /v1/accounts Creates a new account. This account can be used to create accounts for Mint, or intermediary accounts for a Managed Payments customer. For Managed Payments, the account created represents a business entity that will settle stablecoin payments via the customer. Routing for this endpoint is body based. Requests that include the `businessPii` field are routed to the Managed Payments intermediary account creation flow, while requests that omit the `businessPii` field are routed to the standard account creation flow. For the standard account creation flow, If `clientEntityId` is not provided, the account type will be `customer`. If `clientEntityId` is provided, the account type will be `client`. # Get an account Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/get-account /openapi/accounts.yaml get /v1/accounts/{accountId} Retrieves a single account by `accountId`. # List all accounts Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/list-accounts /openapi/accounts.yaml get /v1/accounts Retrieves the accounts available to the calling entity. # Create a recipient Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/create-address-book-recipient /openapi/payouts.yaml post /v1/addressBook/recipients Creates an address book recipient. Required fields depend on your Circle entity; use the request body options that match your integration. Validation failures can return error codes in the `2024`–`2037` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing). # Delete a recipient Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/delete-address-book-recipient /openapi/payouts.yaml delete /v1/addressBook/recipients/{id} # Get a recipient Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/get-address-book-recipient /openapi/payouts.yaml get /v1/addressBook/recipients/{id} # List all recipients Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/list-address-book-recipients /openapi/payouts.yaml get /v1/addressBook/recipients # Modify a recipient Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/modify-address-book-recipient /openapi/payouts.yaml patch /v1/addressBook/recipients/{id} Updates address book recipient metadata. # Borrow against line of credit Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/create-managed-payments-credit-transfer /openapi/managed-payments.yaml post /v1/managedPayments/credit/lines/{lineId}/transfers Initiates a transfer (borrowing) against the line of credit. # Get credit line details Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-line /openapi/managed-payments.yaml get /v1/managedPayments/credit/lines Retrieves the details of the Managed Payments credit line. # Get a credit transfer Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-transfer /openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/transfers/{transferId} Returns detailed information about a specific credit transfer. Fields `outstanding`, `dueDate`, and `disbursedDate` are present when the transfer status is `disbursed`, `paid`, or `past_due`. Field `paidDate` is present only when status is `paid`. # Get repayment wire instructions Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-wire-instructions /openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/wireInstructions Fetches the necessary wire transfer instructions for repaying funds borrowed against the line of credit. # List credit transfers Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/list-managed-payments-credit-transfers /openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/transfers Returns a list of credit transfers for the specified credit line. Filterable by status and date range. # Get an account bank deposit Source: https://developers.circle.com/api-reference/cpn/managed-payments/deposits/get-account-deposit /openapi/accounts.yaml get /v1/accounts/deposits/{id} Returns a bank deposit by ID. # List all account bank deposits Source: https://developers.circle.com/api-reference/cpn/managed-payments/deposits/list-account-deposits /openapi/accounts.yaml get /v1/accounts/deposits Searches for bank deposits sent to accounts. If the date parameters are omitted, returns the most recent deposits. This endpoint returns up to 50 deposits in descending chronological order or pageSize, if provided. # Create a payment intent Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/create-payment-intent /openapi/payments.yaml post /v1/paymentIntents Create a continuous (default) or transient payment intent. Continuous payment intents are created by default. To create a transient payment intent, the type field must be explicitly set to 'transient'. # Expire a payment intent Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/expire-payment-intent /openapi/payments.yaml post /v1/paymentIntents/{id}/expire # Get a payment intent Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/get-payment-intent /openapi/payments.yaml get /v1/paymentIntents/{id} # List all payment intents Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/list-payment-intents /openapi/payments.yaml get /v1/paymentIntents # Refund a payment intent Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/refund-payment-intent /openapi/payments.yaml post /v1/paymentIntents/{id}/refund # Get a payment Source: https://developers.circle.com/api-reference/cpn/managed-payments/payments/get-payment /openapi/payments.yaml get /v1/payments/{id} # List all payments Source: https://developers.circle.com/api-reference/cpn/managed-payments/payments/list-payments /openapi/payments.yaml get /v1/payments # Create a payout Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/create-payout /openapi/payouts.yaml post /v1/payouts Create a stablecoin payout. The following table includes the supported pairs of `amount.currency` and `toAmount.currency` for stablecoin address book payouts: | amount.currency | toAmount.currency | | ---------------- | ----------------- | | USD | USD | | EUR | EUR | Required fields depend on your Circle entity; use the request body options that match your integration. For Singapore (CIRCLE_SG) entities, `purposeOfTransfer` is **required** and must use a payment reason code from [Crypto Payouts payment reason codes](https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes). Invalid or missing values can return error code `5020`. # Get a payout Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/get-payout /openapi/payouts.yaml get /v1/payouts/{id} # List all payouts Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/list-payouts /openapi/payouts.yaml get /v1/payouts # Create a wire bank account Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/create-account-wire-account /openapi/accounts.yaml post /v1/banks/wires Create a bank account for wire transfers. # Get a wire bank account Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/get-account-wire-account /openapi/accounts.yaml get /v1/banks/wires/{id} Retrieves a specific wire bank account. # Get wire instructions Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/get-account-wire-account-instructions /openapi/accounts.yaml get /v1/banks/wires/{id}/instructions Retrieves wire transfer instructions for a specific bank account. # List all wire bank accounts Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/list-account-wire-accounts /openapi/accounts.yaml get /v1/banks/wires Retrieves a list of bank accounts for wire transfers. # Create an account bank withdrawal Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal /openapi/accounts.yaml post /v1/accounts/withdrawals Create a bank withdrawal from an account. This converts a digital asset to fiat currency and sends it to the specified destination bank account. # Get an account bank withdrawal Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/get-account-withdrawal /openapi/accounts.yaml get /v1/accounts/withdrawals/{id} Retrieves a specific bank withdrawal. # List all account bank withdrawals Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/list-account-withdrawals /openapi/accounts.yaml get /v1/accounts/withdrawals Lists all bank withdrawals for accounts. # Create a webhook subscription Source: https://developers.circle.com/api-reference/gateway/all/create-permissionless-subscription /openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless Create a permissionless webhook subscription by configuring an endpoint to receive event notifications. Specify the environment, wallet addresses to monitor, blockchain domains to watch, and event types to receive. # Create a transfer attestation for transferring tokens Source: https://developers.circle.com/api-reference/gateway/all/create-transfer-attestation /openapi/gateway.yaml post /v1/transfer Generates a transfer attestation and operator signature for transferring tokens between domains # Delete a webhook subscription Source: https://developers.circle.com/api-reference/gateway/all/delete-permissionless-subscription /openapi/gateway.yaml delete /v2/notifications/subscriptions/permissionless/{id} Delete an existing permissionless webhook subscription. # Estimate fees and expiration block heights for a transfer Source: https://developers.circle.com/api-reference/gateway/all/estimate-transfer /openapi/gateway.yaml post /v1/estimate Calculates the required fees and expiration block heights for a transfer without requiring signatures or executing the transaction. # Get pending deposits for specified addresses Source: https://developers.circle.com/api-reference/gateway/all/get-deposits /openapi/gateway.yaml post /v1/deposits Returns pending deposits for each specified depositor address across different domains where that address is valid. # Get Gateway info for supported domains and tokens Source: https://developers.circle.com/api-reference/gateway/all/get-gateway-info /openapi/gateway.yaml get /v1/info Provides information about the API and details of the supported domains and tokens. # Get a notification signature public key Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-notification-signature /openapi/gateway.yaml get /v2/notifications/publicKey/{id} Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle. In the headers of each webhook, you can find 1. `X-Circle-Signature`: a header containing the digital signature generated by Circle. 2. `X-Circle-Key-Id`: a header containing the UUID. This value is used as the `ID` URL parameter to retrieve the relevant public key. # Retrieve a webhook subscription Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-subscription /openapi/gateway.yaml get /v2/notifications/subscriptions/permissionless/{id} Retrieve an existing permissionless webhook subscription. # Get all webhook subscriptions Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-subscriptions /openapi/gateway.yaml get /v2/notifications/subscriptions/permissionless Retrieve an array of existing permissionless webhook subscriptions. # Get supported x402 payment kinds Source: https://developers.circle.com/api-reference/gateway/all/get-supported-x402payment-kinds /openapi/gateway.yaml get /v1/x402/supported Returns the payment kinds supported by Circle Gateway for x402 batching. Each kind includes the GatewayWallet contract address in `extra.verifyingContract` which clients use for EIP-712 signing, and an `extra.assets` array containing the supported tokens with their addresses, symbols, and decimals. # Get token balances for specified addresses Source: https://developers.circle.com/api-reference/gateway/all/get-token-balances /openapi/gateway.yaml post /v1/balances Returns the current available balance of each specified address across different domains where that address is valid # Get a transfer by ID Source: https://developers.circle.com/api-reference/gateway/all/get-transfer-by-id /openapi/gateway.yaml get /v1/transfer/{id} Returns detailed information about a transfer. # Get full TransferSpec by transferSpecHash Source: https://developers.circle.com/api-reference/gateway/all/get-transfer-spec /openapi/gateway.yaml get /v1/transferSpec/{transferSpecHash} Retrieve the full TransferSpec for a given transferSpecHash. # Get an x402 transfer by ID Source: https://developers.circle.com/api-reference/gateway/all/get-x402transfer-by-id /openapi/gateway.yaml get /v1/x402/transfers/{id} Retrieves a single x402 transfer by its unique identifier. # Search x402 transfers Source: https://developers.circle.com/api-reference/gateway/all/search-x402transfers /openapi/gateway.yaml get /v1/x402/transfers Returns a paginated list of x402 transfers matching the given filters. Supports cursor-based pagination via pageAfter / pageBefore. # Send a test notification Source: https://developers.circle.com/api-reference/gateway/all/send-permissionless-subscription-test-notification /openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless/{id}/test Send a test notification to the subscriber endpoint. The notification has notificationType "webhooks.test". # Settle an x402 payment Source: https://developers.circle.com/api-reference/gateway/all/settle-x402payment /openapi/gateway.yaml post /v1/x402/settle Settles an x402 payment by submitting the EIP-3009 authorization. The authorization will be verified, the sender's balance locked, and the transaction queued for batch processing. # Submit an EIP-3009 authorization to be batched Source: https://developers.circle.com/api-reference/gateway/all/submit-batch-authorization /openapi/gateway.yaml post /v1/batch/submit Submit a single-chain transfer authorization using EIP-3009 signature. The authorization will be verified, the sender's balance locked, and the transaction queued for batch processing. # Test subscription connection Source: https://developers.circle.com/api-reference/gateway/all/test-permissionless-subscription-connection /openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless/{id}/testConnection Verify that the subscriber endpoint for the given subscription is reachable. # Update a webhook subscription Source: https://developers.circle.com/api-reference/gateway/all/update-permissionless-subscription /openapi/gateway.yaml patch /v2/notifications/subscriptions/permissionless/{id} Update a webhook subscription. Metadata fields can be updated independently. To update filters, provide `notificationTypes`, `addresses`, and `domains` together; those fields fully replace the existing filters. # Verify an x402 payment payload Source: https://developers.circle.com/api-reference/gateway/all/verify-x402payment /openapi/gateway.yaml post /v1/x402/verify Verifies that an x402 payment payload can be processed by running all read-only validation checks (scheme, network, token, signature, temporal constraints, address/amount matching). A valid result does not guarantee settlement — balance and nonce checks only happen at settle time. # Idempotent Requests Source: https://developers.circle.com/api-reference/idempotent-requests Idempotency keys let you safely retry Circle API calls. Circle APIs support [idempotent requests](https://en.wikipedia.org/wiki/Idempotence), so making the same request multiple times produces the same result. This lets you safely retry API calls if something goes wrong. ## Idempotency keys Certain endpoints require you to generate an idempotency key to identify the request. For endpoints that require an idempotency key, each request must have a unique key. The server uses this key to identify a specific request. When a request is made with the same idempotency key, the server returns the original response instead of executing the operation again. For endpoints that require it, the idempotency key must be in [UUID version 4](https://en.wikipedia.org/wiki/Universally_unique_identifier) format. The following example demonstrates how to generate an idempotency key in Node.js: ```typescript theme={null} import crypto from "crypto"; function generateIdempotencyKey(): string { return crypto.randomUUID(); } const idempotencyKey: string = generateIdempotencyKey(); console.log(idempotencyKey); // e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479" ``` # API Keys Source: https://developers.circle.com/api-reference/keys Learn about the different types of API keys used to authenticate requests to Circle's platform. Certain products use API keys to authenticate requests. Circle provides three types of keys for different use cases: [API keys](#api-keys) for server-side access, [client keys](#client-keys) for frontend applications, and [kit keys](#kit-keys) for SDK integrations. Permissionless products like CCTP and Gateway do not require an API key. Authenticate server-side requests to Circle's RESTful APIs. Authenticate client applications with domain or app binding. Required for frontend SDKs. Authenticate kit access with a single key that works on both testnet and mainnet. ## API keys An API key is a unique string used to authenticate and enable access to privileged operations on Circle's APIs. It's required for any RESTful API requests to Circle services. Without it, requests will fail. ### Keep your API keys safe API keys allow access to sensitive operations, so you must secure them. * **Avoid public exposure**: Never share API keys or include them in client-side code, public repositories, or other public mediums. * **Manage securely**: Use the Circle Console to generate and manage API keys. When generating a key, copy it exactly as displayed. Losing control of your API key can result in financial loss. ### API key authentication Use the headers below to authenticate requests on testnet or mainnet. #### Testnet authorization header example ```text theme={null} authorization: Bearer TEST_API_KEY:ebb3ad72232624921abc4b162148bb84:019ef3358ef9cd6d08fc32csfe89a68d ``` #### Mainnet authorization header example ```text theme={null} authorization: Bearer LIVE_API_KEY:ebb3ad72232624921abc4b162148bb84:019ef3358ef9cd6d08fc32csfe89a68d ``` ### Test authentication To verify your API key setup, use the following `curl` command to retrieve wallets: ```bash theme={null} curl --request GET \ --url https://api.circle.com/v1/w3s/wallets \ --header 'accept: application/json' \ --header 'authorization: Bearer ' ``` A successful response looks like this: ```json theme={null} { "data": { "wallets": [] } } ``` An error response looks like this: ```json theme={null} { "code": 401, "message": "Malformed authorization. Are the credentials properly encoded?" } ``` *** ## Client keys A client key is a unique string used to authenticate and authorize API access for apps using Circle's SDKs. A client key is linked to either a specific host domain (websites), bundle ID (iOS), or package name (Android). This restricts access to pre-configured apps. A client key must be included in the headers of all modular wallets SDK API calls. ### Best practices for client keys Client keys enable access to sensitive application operations, so protecting them is critical. Follow these best practices: 1. **Use separate keys for each application**: Create separate keys for web and mobile apps (iOS, Android) to prevent shared vulnerabilities. 2. **Monitor for misuse**: Set up alerts for unusual activity, such as unexpected spikes in API calls, and use monitoring tools to detect anomalies. 3. **Rotate keys regularly**: Regenerate client keys periodically and update them in your apps to reduce risk if a key is compromised. 4. **Store keys securely**: Use secure storage options like Local Storage or Secure Storage for mobile apps, and avoid unnecessary exposure. 5. **Restrict access**: Limit the scope of client keys by associating them with specific apps or domains to minimize potential misuse. *** ## Kit keys A kit key is a unique string used to authenticate access for Circle's developer kits. Kit keys simplify integration by providing a single credential that works across both testnet and mainnet environments, reducing configuration overhead when building. Kit keys are free to create and do not require KYC. **Testnet and mainnet compatibility** Unlike API keys and client keys, kit keys work on both testnet and mainnet. You can use the same key during development and in production. ### Keep your kit keys safe Kit keys enable access to SDK features, so protecting them is essential. * **Avoid public exposure**: Never share kit keys or include them in client-side code, public repositories, or other public mediums. * **Manage securely**: Use your [Circle Developer account](https://console.circle.com/api-keys) to generate and manage kit keys. When generating a key, copy it exactly as displayed. Losing control of your kit key can result in unauthorized access to SDK capabilities. # Maker deliver operation failed Source: https://developers.circle.com/api-reference/stablefx/all/contract-maker-deliver-failed /openapi/stablefx.yaml webhook contractMakerDeliverFailed The maker deliver operation on the contract has failed. The maker's deliver transaction failed to confirm onchain. # Record trade operation failed Source: https://developers.circle.com/api-reference/stablefx/all/contract-record-trade-failed /openapi/stablefx.yaml webhook contractRecordTradeFailed The record trade operation on the contract has failed. The initial onchain recording of the trade was unsuccessful. # Taker deliver operation failed Source: https://developers.circle.com/api-reference/stablefx/all/contract-taker-deliver-failed /openapi/stablefx.yaml webhook contractTakerDeliverFailed The taker deliver operation on the contract has failed. The taker's deliver transaction failed to confirm onchain. # Create a quote Source: https://developers.circle.com/api-reference/stablefx/all/create-quote /openapi/stablefx.yaml post /v1/exchange/stablefx/quotes Creates a quote for a trade between two currencies. You should provide an `amount` for the `from` parameter or the `to` parameter, but not for both. Use `type:tradable` for an executable quote or `type:reference` for an indicative quote. Tradable quotes include presign `typedData` for signing. # Create a webhook subscription Source: https://developers.circle.com/api-reference/stablefx/all/create-subscription /openapi/stablefx.yaml post /v2/stablefx/notifications/subscriptions Create a webhook subscription by configuring an endpoint to receive notifications. # Create a trade Source: https://developers.circle.com/api-reference/stablefx/all/create-trade /openapi/stablefx.yaml post /v1/exchange/stablefx/trades Accepts a quote and creates a trade. # Delete a notification subscription Source: https://developers.circle.com/api-reference/stablefx/all/delete-subscription /openapi/stablefx.yaml delete /v2/stablefx/notifications/subscriptions/{id} Delete an existing subscription. # Fund trades Source: https://developers.circle.com/api-reference/stablefx/all/fund-trade /openapi/stablefx.yaml post /v1/exchange/stablefx/fund Executes funding for trades using Permit2 signatures. This endpoint relays the signed permit data to complete the funding operation for trades. # Generate funding presign data Source: https://developers.circle.com/api-reference/stablefx/all/generate-funding-presign-data /openapi/stablefx.yaml post /v1/exchange/stablefx/signatures/funding/presign Returns the Permit2 EIP-712 payload that the trader must sign for funding operations. `fundingMode` net option is supported for maker funding requests. # Generate trade presign data Source: https://developers.circle.com/api-reference/stablefx/all/generate-trade-signature-data /openapi/stablefx.yaml get /v1/exchange/stablefx/signatures/presign/{tradeId} Returns the EIP-712 payload that the trader must sign for a CPS trade. # Get a notification signature public key Source: https://developers.circle.com/api-reference/stablefx/all/get-notification-signature /openapi/stablefx.yaml get /v2/stablefx/notifications/publicKey/{id} Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle. In the headers of each webhook, you can find - `X-Circle-Signature`: a header containing the digital signature generated by Circle. - `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key. # Get a notification subscription Source: https://developers.circle.com/api-reference/stablefx/all/get-subscription /openapi/stablefx.yaml get /v2/stablefx/notifications/subscriptions/{id} Returns an existing notification subscription. # Get all webhook subscriptions Source: https://developers.circle.com/api-reference/stablefx/all/get-subscriptions /openapi/stablefx.yaml get /v2/stablefx/notifications/subscriptions Returns an array of all webhook subscriptions. # Get a trade Source: https://developers.circle.com/api-reference/stablefx/all/get-trade-by-id /openapi/stablefx.yaml get /v1/exchange/stablefx/trades/{tradeId} Returns a trade specified by the ID path parameter # Get fee for a trade Source: https://developers.circle.com/api-reference/stablefx/all/get-trade-fee /openapi/stablefx.yaml get /v1/exchange/stablefx/fees/{tradeId} Returns the fee associated with the trade ID provided in the path parameter. # Get all trades Source: https://developers.circle.com/api-reference/stablefx/all/list-trades /openapi/stablefx.yaml get /v1/exchange/stablefx/trades Returns a paginated list of all trades. # Register a trade signature Source: https://developers.circle.com/api-reference/stablefx/all/register-trade-signature /openapi/stablefx.yaml post /v1/exchange/stablefx/signatures Registers a signed EIP-712 payload from the trader that confirms trade intent. # Trade breached Source: https://developers.circle.com/api-reference/stablefx/all/trade-breached /openapi/stablefx.yaml webhook tradeBreached The StableFX trade has breached its maturity date. # Trade completed Source: https://developers.circle.com/api-reference/stablefx/all/trade-completed /openapi/stablefx.yaml webhook tradeCompleted The StableFX trade has been completed successfully. Both the maker and the taker have funded their side and the trade is fully settled. # Trade confirmed Source: https://developers.circle.com/api-reference/stablefx/all/trade-confirmed /openapi/stablefx.yaml webhook tradeConfirmed The StableFX trade has been confirmed by the exchange. # Trade failed Source: https://developers.circle.com/api-reference/stablefx/all/trade-failed /openapi/stablefx.yaml webhook tradeFailed The StableFX trade has failed. # Trade maker funded Source: https://developers.circle.com/api-reference/stablefx/all/trade-maker-funded /openapi/stablefx.yaml webhook tradeMakerFunded The maker has funded their side of the trade. The maker's fund delivery transaction has been confirmed onchain. # Trade pending settlement Source: https://developers.circle.com/api-reference/stablefx/all/trade-pending-settlement /openapi/stablefx.yaml webhook tradePendingSettlement The StableFX trade has been confirmed onchain and is awaiting funding from taker and maker. # Trade refunded Source: https://developers.circle.com/api-reference/stablefx/all/trade-refunded /openapi/stablefx.yaml webhook tradeRefunded The StableFX trade has been refunded. # Trade taker funded Source: https://developers.circle.com/api-reference/stablefx/all/trade-taker-funded /openapi/stablefx.yaml webhook tradeTakerFunded The taker has funded their side of the trade. The taker's fund delivery transaction has been confirmed onchain. # Update a notification subscription Source: https://developers.circle.com/api-reference/stablefx/all/update-subscription /openapi/stablefx.yaml patch /v2/stablefx/notifications/subscriptions/{id} Update a notification subscription by configuring an endpoint to receive notifications. # Retrieve a transfer Source: https://developers.circle.com/api-reference/wallets/buidl/get-transfer /openapi/buidl-wallets.yaml get /v1/w3s/buidl/transfers/{id} Retrieve an existing transfer. # Retrieve a user operation Source: https://developers.circle.com/api-reference/wallets/buidl/get-user-op /openapi/buidl-wallets.yaml get /v1/w3s/buidl/userOps/{id} Retrieve an existing user operation. # List transfers Source: https://developers.circle.com/api-reference/wallets/buidl/list-transfers /openapi/buidl-wallets.yaml get /v1/w3s/buidl/transfers Retrieve a list of transfers that fit the specified parameters. # List user operations Source: https://developers.circle.com/api-reference/wallets/buidl/list-user-ops /openapi/buidl-wallets.yaml get /v1/w3s/buidl/userOps Retrieve a list of all user operations that fit the specified parameters. # Get wallet balances by blockchain and address Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-balances-by-blockchain-address /openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{blockchain}/{address}/balances Retrieve wallet balances by blockchain and address. # Get wallet balances Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-balances-by-id /openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{id}/balances Retrieve the balances of a wallet by its ID. # Get wallet NFTs by blockchain and address Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-nfts-by-blockchain-address /openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{blockchain}/{address}/nfts Retrieve the NFTs of a wallet by applicable blockchain and address. # Get wallet NFTs Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-nfts-by-id /openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{id}/nfts Retrieve the NFTs of a wallet by its ID. # Create a notification subscription Source: https://developers.circle.com/api-reference/wallets/common/create-subscription /openapi/configurations_2.yaml post /v2/notifications/subscriptions Create a notification subscription by configuring an endpoint to receive notifications. For details, see the [Notification Flows](https://developers.circle.com/wallets/webhook-notification-flows) guide. # Delete a notification subscription Source: https://developers.circle.com/api-reference/wallets/common/delete-subscription /openapi/configurations_2.yaml delete /v2/notifications/subscriptions/{id} Delete an existing subscription. # Get a notification signature public key Source: https://developers.circle.com/api-reference/wallets/common/get-notification-signature /openapi/configurations_2.yaml get /v2/notifications/publicKey/{id} Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle. In the headers of each webhook, you can find 1. `X-Circle-Signature`: a header containing the digital signature generated by Circle. 2. `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key. # Retrieve a notification subscription Source: https://developers.circle.com/api-reference/wallets/common/get-subscription /openapi/configurations_2.yaml get /v2/notifications/subscriptions/{id} Retrieve an existing notification subscription. # Get all notification subscriptions Source: https://developers.circle.com/api-reference/wallets/common/get-subscriptions /openapi/configurations_2.yaml get /v2/notifications/subscriptions Retrieve an array of existing notification subscriptions. # Ping Source: https://developers.circle.com/api-reference/wallets/common/ping /openapi/configurations_2.yaml get /ping Checks that the service is running. # Update a notification subscription Source: https://developers.circle.com/api-reference/wallets/common/update-subscription /openapi/configurations_2.yaml patch /v2/notifications/subscriptions/{id} Update subscription endpoint to receive notifications. # Screen a blockchain address Source: https://developers.circle.com/api-reference/wallets/compliance/screen-address /openapi/compliance.yaml post /v1/w3s/compliance/screening/addresses Create a screening request for a specific blockchain address and chain. # Accelerate a transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-accelerate /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/{id}/accelerate Accelerates a specified transaction from a developer-controlled wallet. Additional gas fees may be incurred. # Cancel a transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-cancel /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/{id}/cancel Cancels a specified transaction from a developer-controlled wallet. Gas fees may still be incurred. This is a best-effort operation, it won't be effective if the original transaction has already been processed by the blockchain. # Create a contract execution transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-contract-execution /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/contractExecution Creates a transaction which executes a smart contract. ABI parameters must be passed in the request. Related transactions may be submitted as a batch transaction in a single call. You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body. # Create a transfer transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-transfer /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/transfer Initiates an on-chain digital asset transfer from a specified developer-controlled wallet. You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body. # Create a wallet upgrade transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-wallet-upgrade /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/walletUpgrade Creates a transaction which upgrades a wallet. # Estimate fee for a contract execution transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-transaction-estimate-fee /openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/contractExecution/estimateFee Estimates gas fees that will be incurred for a contract execution transaction, given its ABI parameters and blockchain. # Estimate fee for a transfer transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-transfer-estimate-fee /openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/transfer/estimateFee Estimates gas fees that will be incurred for a transfer transaction; given its amount, blockchain, and token. # Validate an address Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-validate-address /openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/validateAddress Confirms that a specified address is valid for a given token on a certain blockchain. # Create wallets Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/wallets Creates a new developer-controlled wallet or a batch of wallets within a wallet set, given the target blockchain and wallet name. **Note:** Each `walletSetId` supports a maximum of 10 million wallets. # Create a new wallet set Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet-set /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/walletSets Creates a new developer-controlled wallet set. **Note:** A developer account can create up to 1,000 wallet sets, with each set supporting up to 10 million wallets. To ensure EVM wallets are created with the same address across chains, see [Unified Wallet Addressing on EVM Chains](/w3s/unified-wallet-addressing-evm). # Derive a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/derive-wallet /openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/wallets/{id}/blockchains/{blockchain} Derives an EOA (Externally Owned Account) or SCA (Smart Contract Account) wallet using the address of the specified wallet and blockchain. If the target wallet already exists, its metadata will be updated with the provided metadata. This operation is only supported for EVM-based blockchains. # Derive wallet by address Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/derive-wallet-by-address /openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/wallets/derive Creates a wallet on the target blockchain using the same address as the source wallet identified by source blockchain and wallet address. If the target wallet already exists, its metadata is updated. # Get fee parameters of a blockchain Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-fee-parameters /openapi/developer-controlled-wallets.yaml get /v1/w3s/developer/transactions/feeParameters Get latest fee parameters of a blockchain with an optional account type (default to 'EOA'). # Get the lowest nonce pending transaction for a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-lowest-nonce-transaction /openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions/lowestNonceTransaction For a nonce-supported blockchain, get the lowest nonce transaction that's in QUEUED or SENT or STUCK state for the provided wallet. # Get token details Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-token-id /openapi/developer-controlled-wallets.yaml get /v1/w3s/tokens/{id} Fetches details of a specific token given its unique identifier. Every token in your network of wallets has a UUID associated with it, regardless of whether it's already recognized or was added as a monitored token. # Get a transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction /openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions/{id} Retrieves info for a single transaction using it's unique identifier. # Retrieve a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet /openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id} Retrieve an existing wallet # Get a wallet set Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet-set /openapi/developer-controlled-wallets.yaml get /v1/w3s/walletSets/{id} Retrieve an existing wallet set. # Get all wallet sets Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet-sets /openapi/developer-controlled-wallets.yaml get /v1/w3s/walletSets Retrieve an array of existing wallet sets. # List wallets Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets /openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets Retrieves a list of all wallets that fit the specified parameters. # List wallets with balances Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets-with-balances /openapi/developer-controlled-wallets.yaml get /v1/w3s/developer/wallets/balances Retrieves a list of all wallets that match the specified parameters. Wallet balances update automatically after each transfer. **Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes. # List transactions Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-transactions /openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions Lists all transactions. Includes details such as status, source/destination, and transaction hash. # Get token balance for a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-balance /openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id}/balances Fetches the digital asset balance for a single developer-controlled wallet using its unique identifier. **Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes. # Get NFTs for a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-nfts /openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id}/nfts Fetches the info for all NFTs stored in a single developer-controlled wallet, using the wallets unique identifier. # Sign delegate action Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-delegate-action /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/delegateAction Sign a delegate action from a specific developer-controlled wallet. NOTE: This endpoint is only available for NEAR and NEAR-TESTNET. # Sign message Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-message /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/message Sign a message from a specified developer-controlled wallet. This endpoint supports message signing for Ethereum-based blockchains (using EIP-191), Solana and Aptos (using Ed25519 signatures). Note that Smart Contract Accounts (SCA) are specific to Ethereum and EVM-compatible chains. The difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/." You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body. # Sign transaction Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-transaction /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/transaction Sign a transaction from a specific developer-controlled wallet. You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body. NOTE: This endpoint is only available for the following chains: `SOL`, `SOL-DEVNET`, `NEAR`, `NEAR-TESTNET`, `EVM`, `EVM-TESTNET`. Each chain defines its own standard, please refer to [Signing APIs doc](https://learn.circle.com/w3s/signing-apis). # Sign typed data Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-typed-data /openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/typedData | Sign the EIP-712 typed structured data from a specified developer-controlled wallet. You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body. This endpoint only supports Ethereum and EVM-compatible blockchains. Please note that not all apps currently support Smart Contract Accounts (SCA); the difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum apps that support SCA: https://eip1271.io/. # Update a wallet Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/update-wallet /openapi/developer-controlled-wallets.yaml put /v1/w3s/wallets/{id} Updates info metadata of a wallet. # Update a wallet set Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/update-wallet-set /openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/walletSets/{id} Update the name of the wallet set # Set monitored tokens Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/create-monitored-tokens /openapi/configurations_1.yaml post /v1/w3s/config/entity/monitoredTokens Add a new token to the monitored token list. # Delete monitored tokens Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/delete-monitored-tokens /openapi/configurations_1.yaml post /v1/w3s/config/entity/monitoredTokens/delete Delete tokens from the monitored token list. # Get configuration for entity Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/get-entity-config /openapi/configurations_1.yaml get /v1/w3s/config/entity Get the app ID associated to the entity. # Get public key for entity Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/get-public-key /openapi/configurations_1.yaml get /v1/w3s/config/entity/publicKey Get the public key associated with the entity. # Retrieve existing monitored tokens. Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/list-monitored-tokens /openapi/configurations_1.yaml get /v1/w3s/config/entity/monitoredTokens Get monitored tokens # Request testnet tokens Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/request-testnet-tokens /openapi/configurations_1.yaml post /v1/faucet/drips Request testnet tokens for your wallet. **Note:** Calling the `/v1/faucet/drips` API requires upgrading to mainnet. # Update monitored tokens Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/update-monitored-tokens /openapi/configurations_1.yaml put /v1/w3s/config/entity/monitoredTokens Upsert the monitored token list. # Update monitored tokens scope Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/update-monitored-tokens-scope /openapi/configurations_1.yaml put /v1/w3s/config/entity/monitoredTokens/scope Select between monitoring all tokens or selected tokens added to the monitored tokens list. # Get a deviceToken to log in with email OTP Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-device-token-email-login /openapi/user-controlled-wallets.yaml post /v1/w3s/users/email/token Get a deviceToken to login with email OTP in SDK # Get deviceToken to perform social login Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-device-token-social-login /openapi/user-controlled-wallets.yaml post /v1/w3s/users/social/token Get deviceToken to perform social login in SDK # Estimate fee for a contract execution transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-transaction-estimate-fee /openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/contractExecution/estimateFee Estimates gas fees that will be incurred for a contract execution transaction, given its ABI parameters and blockchain. # Estimate fee for a transfer transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-transfer-estimate-fee /openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/transfer/estimateFee Estimates gas fees that will be incurred for a transfer transaction; given its amount, blockchain, and token. # Create a user Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user /openapi/user-controlled-wallets.yaml post /v1/w3s/users Create a user. # Create a challenge for PIN setup Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-pin-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/pin Creates a challenge for PIN setup without setting up the wallets. # Create a challenge for PIN restore Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-pin-restore-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/pin/restore Creates a challenge to restore a user's PIN using security questions. # Create a Challenge to accelerate a transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-accelerate-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/{id}/accelerate Generates a challenge to accelerate a specific transaction from a user-controlled wallet. Additional gas fees may apply. # Create a challenge to cancel a transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-cancel-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/{id}/cancel Generates a challenge to cancel a specific transaction from a user-controlled wallet. Gas fees may still apply. # Create a challenge for contract execution Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-contract-execution-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/contractExecution Generates a challenge for creating a transaction which executes a smart contract. ABI parameters must be passed in the request. # Create a challenge for a transfer Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-transfer-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/transfer Generates a challenge for initiating an on-chain digital asset transfer from a specified user-controlled wallet # Create a challenge for a wallet upgrade Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-wallet-upgrade-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/walletUpgrade Generates a challenge to create a transaction that upgrades a wallet. # Create wallets Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-wallet /openapi/user-controlled-wallets.yaml post /v1/w3s/user/wallets Generates a challenge to create a new user-controlled wallet or a batch of wallets. You must specify the blockchain and wallet name. # Create a challenge for user initialization with wallet creation Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-with-pin-challenge /openapi/user-controlled-wallets.yaml post /v1/w3s/user/initialize Creates a challenge for user initialization and creates one or more wallets. # Validate an address Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-validate-address /openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/validateAddress Confirms that a specified address is valid for a given token on a certain blockchain. # Get the lowest nonce pending transaction for a wallet Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-lowest-nonce-transaction /openapi/user-controlled-wallets.yaml get /v1/w3s/transactions/lowestNonceTransaction For a nonce-supported blockchain, get the lowest nonce transaction that's in QUEUED or SENT or STUCK state for the provided wallet. # Get token details Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-token-id /openapi/user-controlled-wallets.yaml get /v1/w3s/tokens/{id} Fetches details of a specific token given its unique identifier. Every token in your network of wallets has a UUID associated with it, regardless of whether it's already recognized or was added as a monitored token. # Get a transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-transaction /openapi/user-controlled-wallets.yaml get /v1/w3s/transactions/{id} Retrieves info for a single transaction using it's unique identifier. # Get a user by ID Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user /openapi/user-controlled-wallets.yaml get /v1/w3s/users/{id} Get user by ID. # Get user Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-by-token /openapi/user-controlled-wallets.yaml get /v1/w3s/user Retrieve the user by token. # Get a challenge Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-challenge /openapi/user-controlled-wallets.yaml get /v1/w3s/user/challenges/{id} Retrieve a user challenge. # Create a user token Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-token /openapi/user-controlled-wallets.yaml post /v1/w3s/users/token Generate user session and SDK secret key. # Get a wallet Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-wallet /openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id} Retrieves info for a single user-controlled wallet using it's unique identifier. # List transactions Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-transactions /openapi/user-controlled-wallets.yaml get /v1/w3s/transactions Lists all transactions. Includes details such as status, source/destination, and transaction hash. # List challenges Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-user-challenges /openapi/user-controlled-wallets.yaml get /v1/w3s/user/challenges List all challenges by status for a user. # List users Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-users /openapi/user-controlled-wallets.yaml get /v1/w3s/users Get all the users under the entity. # Get token balance for a wallet Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallet-balance /openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id}/balances Fetches the digital asset balance for a single user-controlled wallet using its unique identifier. **Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes. # Get NFTs for a wallet Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallet-nfts /openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id}/nfts Fetches the info for all NFTs stored in a single user-controlled wallet, using the wallets unique identifier. # List wallets Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallets /openapi/user-controlled-wallets.yaml get /v1/w3s/wallets Retrieves a list of all user-controlled wallets that fit the specified parameters. # Get a new userToken with the refreshToken Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/refresh-user-token /openapi/user-controlled-wallets.yaml post /v1/w3s/users/token/refresh Get a new userToken with the refreshToken passed over from sdk/performLogin which matches to the current userToken # Resend an OTP email to the user Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/resend-otp /openapi/user-controlled-wallets.yaml post /v1/w3s/users/email/resendOTP When the users don’t receive the OTP email, you can call this API to resend OTP email. The prior OTP email would expire after the new one is sent out. # Create a challenge to sign message Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-message /openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/message Generates a challenge for signing a message from a specified user-controlled wallet. This endpoint supports Ethereum-based blockchains (using EIP-191), Solana and Aptos (using Ed25519 signatures). Note that Smart Contract Accounts (SCA) are specific to Ethereum and EVM-compatible chains. The difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/. # Create a challenge to sign transaction Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-transaction /openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/transaction Generate a challenge for signing the transaction from a specific user-controlled wallet. NOTE: This endpoint supports the following blockchains: SOL, SOL-DEVNET, EVM, EVM-TESTNET. Each chain defines its own standard. For more details, see [Signing APIs](https://developers.circle.com/w3s/signing-apis). # Create a challenge to sign typed data Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-typed-data /openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/typedData Generates a challenge for signing the EIP-712 typed structured data from a specified user-controlled wallet. This endpoint only supports Ethereum and EVM-compatible blockchains. Please note that not all Dapps currently support Smart Contract Accounts (SCA); the difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/. # Create a challenge to update PIN Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/update-user-pin-challenge /openapi/user-controlled-wallets.yaml put /v1/w3s/user/pin Creates a challenge to update a user's PIN using the current PIN. # Update a wallet Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/update-wallet /openapi/user-controlled-wallets.yaml put /v1/w3s/wallets/{id} Updates info for a single user-controlled wallet using it's unique identifier. # Get an attestation for a crosschain transfer Source: https://developers.circle.com/api-reference/xreserve/all/get-attestation /openapi/xreserve.yaml get /v1/attestations/{depositMessageHash} Returns the attestation for a specified crosschain transfer using the deposit message hash. # Get attestations by source transaction hash Source: https://developers.circle.com/api-reference/xreserve/all/get-attestations-by-tx-hash /openapi/xreserve.yaml get /v1/attestations Returns all attestations associated with the specified source chain transaction hash. A single transaction may produce multiple attestations across different remote domains. # Get token balances on a remote domain Source: https://developers.circle.com/api-reference/xreserve/all/get-balances /openapi/xreserve.yaml get /v1/balances/{remoteDomain} Returns the expected token balances for a specified remote domain based on the deposit amounts made into xReserve. # Get domain information Source: https://developers.circle.com/api-reference/xreserve/all/get-info /openapi/xreserve.yaml get /v1/info Returns information on source and remote domains, including its supported tokens and configuration details. # Get withdrawal status Source: https://developers.circle.com/api-reference/xreserve/all/get-withdrawal-status /openapi/xreserve.yaml get /v1/withdrawal/{withdrawalId} Returns the status and transfer details of a specified withdrawal group. # List attestations for crosschain transfers Source: https://developers.circle.com/api-reference/xreserve/all/list-attestations /openapi/xreserve.yaml get /v1/remote-domains/{remoteDomain}/attestations Returns an array of attestations for the specified crosschain transfers, filtered by remote domain. Use the Link header in the response to navigate between pages. # Prepare a withdrawal request Source: https://developers.circle.com/api-reference/xreserve/all/prepare-withdrawal /openapi/xreserve.yaml post /v1/prepare-withdrawal Turns the user's burn transaction data on the remote chain into fully encoded burn intents to send to the `/withdraw` endpoint. This endpoint performs the following: - Resolves contract and token addresses across networks. - Determines the optimal forwarding strategy, such as redepositing to another xReserve remote domain or forwarding to a CCTP domain. - Calculates transfer amounts and fees. - Encodes forwarding call data with the calculated transfer amounts. - Generates `maxBlockHeight` and `maxFee` values with safety buffers. - Returns data in the structure expected by the withdraw endpoint. Note: The `/withdraw` endpoint requires signatures and the remote chain `burnTxId` in addition to the burn intent prepared by this endpoint. # Submit signed burn intents for withdrawal Source: https://developers.circle.com/api-reference/xreserve/all/submit-withdrawal /openapi/xreserve.yaml post /v1/withdraw Submits up to five signed burn intent batches per request call. Each batch may contain one burn intent or a set of up to 10. # Assets Source: https://developers.circle.com/assets Build on trusted digital money: USDC and EURC for open integration, USYC for yield, and xReserve to issue branded, USDC-backed stablecoins. ## Stablecoins The foundation of programmable money: stable, interoperable assets that move value across borders and blockchains. ## Tokenized money market Circle's USYC provides institutions with 24/7 access to a yield-bearing money market fund settled onchain with real-time subscriptions and redemptions. ## Dive deeper * Follow the [USDC quickstarts](/stablecoins/quickstarts/transfer-usdc-evm) to test transfers on any supported L1 or L2. * Use the [EURC quickstart](/stablecoins/quickstarts/transfer-eurc-evm) to stand up euro rails in minutes. * Explore [USYC subscribe and redeem guides](/tokenized/usyc/subscribe-and-redeem) to wire funds in and out of tokenized Treasuries. * Learn about [Circle Mint](/circle-mint) and how institutions can mint USDC and EURC. # Build Onchain Experiences Source: https://developers.circle.com/build-onchain Compose wallets, contracts, gas sponsorship, and compliance to ship onchain apps faster. ## What you can build * [**Embedded wallets**](/wallets): build flexible, secure, and scalable wallets into your application. * [**Gasless UX**](/wallets/gas-station): give end users a gasless experience with Gas Station and Paymaster. * [**Smart contracts**](/contracts): create, deploy, and execute smart contracts through intuitive APIs. * [**Compliance operations**](/wallets/compliance-engine): screen transactions and automate alerts. ## Compose your stack Mix and match these building blocks based on who controls the keys, how you cover fees, and the level of automation you need. Use APIs to create wallets, move funds, and manage policies on behalf of your users with instant scale. Ship passkeys, social logins, and custom signing UIs so end users truly own their keys across devices. Plug in modules like permissions, recovery, or automation to ship custom smart-accounts. Sponsor gas to create a gasless UX for end-users. Let anyone pay gas directly in USDC through permissionless ERC-4337 paymasters. Deploy and operate smart contracts via audited templates, APIs, and monitoring. Screen transactions, manage rules, and investigate alerts. ## Building something crosschain? Wallets and contracts become more powerful when they connect across ecosystems. Use these primitives to move liquidity crosschain and expose a unified balance. **Use dedicated SDKs for crosschain transfers** [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) and [Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit) let you move USDC across blockchains without low-level protocol work. Both support multiple blockchains and wallet providers. Burn-and-mint native USDC between supported chains with guarantees. Give users a single USDC balance they can spend anywhere while Circle handles settlement. ## Dive deeper * Follow the [dev-controlled wallet quickstart](/wallets/dev-controlled/create-your-first-wallet) to stand up API-driven wallets in minutes. * Use the [user-controlled wallet tutorials](/wallets/user-controlled) to embed passkey flows and branded signing experiences. * Head to the [Contracts quickstart](/contracts/scp-deploy-smart-contract) to deploy an audited template and connect to wallets. * Explore [Gas Station quickstarts](/wallets/gas-station/send-a-gasless-transaction) and [Paymaster guides](/paymaster/pay-gas-fees-usdc) to abstract gas across networks. # Cross-Chain Transfer Protocol Source: https://developers.circle.com/cctp Cross-Chain Transfer Protocol (CCTP) is a permissionless onchain utility that facilitates native USDC transfers across blockchains. CCTP burns USDC on the source blockchain and mints it on the destination blockchain, enabling secure 1:1 transfers without traditional bridge liquidity pools or wrapped tokens. **Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to simplify crosschain transfers with CCTP.** Bridge Kit is a lightweight SDK that uses CCTP as its protocol provider, letting you transfer USDC between blockchains in just a few lines of code. ## Key features Transfer native USDC across blockchains without wrapped tokens or liquidity pools Choose between [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) for speed or [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times) for cost efficiency Trigger automated actions on the destination blockchain after USDC arrives ## What you can build CCTP enables you to build applications that require moving USDC across blockchains. Here are some common use cases: Rebalance USDC holdings across blockchains to meet liquidity demands, manage treasury positions, or take advantage of market opportunities with minimal latency. Enable users to swap tokens on one blockchain for tokens on another blockchain by routing through USDC. Build seamless crosschain trading experiences that feel like a single transaction. Accept USDC payments on one blockchain and automatically transfer funds to another blockchain where your business operations are based or where recipients prefer to receive funds. Use CCTP hooks to chain together crosschain actions. Transfer USDC across blockchains and automatically deposit it into DeFi protocols, purchase NFTs, or execute smart contract logic. ## Get started Build a script to transfer USDC between EVM blockchains using CCTP Transfer USDC from Solana to an EVM blockchain using CCTP Transfer USDC between Arc and Stellar using CCTP ## Related products CCTP and Gateway offer different approaches to crosschain transfers. This table compares the two approaches. | Attribute | CCTP | Gateway | | ------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | **Use case** | Transfer USDC from one blockchain to another | Hold a unified USDC balance accessible on any supported blockchain | | **Transfer speed** | Fast Transfer: \~8-20 seconds
Standard Transfer: 15-19 minutes (Ethereum/L2s) | Instant (\<500 ms) after balance is established | | **Balance model** | Point-to-point transfers | Unified crosschain balance | | **Custody** | Non-custodial | Non-custodial with 7-day trustless withdrawal option | | **Supported blockchains** | [View list](/cctp/concepts/supported-chains-and-domains) | [View list](/gateway/references/supported-blockchains) | # CCTP-Enabled HyperCore Transfers Source: https://developers.circle.com/cctp/concepts/cctp-on-hypercore CCTP lets you transfer USDC from all supported CCTP domains to HyperCore using a `CoreDepositWallet` contract deployed on HyperEVM. The `CoreDepositWallet` handles depositing and withdrawing USDC between HyperEVM and HyperCore. This topic explains how the HyperCore workflow works and covers HyperCore-specific considerations. **Note:** HyperCore balances reflect protocol-level credits, not Circle-issued USDC. Native USDC remains in the `CoreDepositWallet` contract on HyperEVM and withdrawals are required to [redeem native USDC from HyperEVM](/cctp/howtos/withdraw-usdc-from-hypercore-to-evm). ## How it works The HyperCore workflow from source chains that are not HyperEVM is a two-step process: funds are transferred in the standard CCTP workflow to HyperEVM, then forwarded to HyperCore by depositing them into the `CoreDepositWallet` contract. The burn transaction uses one of two contracts depending on the source chain: * `TokenMessengerV2` contract * `CctpExtension` contract The burn transaction includes a hook that calls the `CctpForwarder` contract on HyperEVM to forward the USDC to the recipient address on HyperCore. Here's how the HyperCore deposit workflow works: 1. Check the CCTP API for fees. Fast transfers from Arbitrum with a HyperCore destination have no fast transfer fee. Using Circle's Forwarder Service to forward to HyperCore is optional and has a dynamic destination chain gas fee. 2. Calculate the USDC amounts minus fees. 3. Approve the contract to spend the amount of USDC you want to burn. If you are interacting with the `TokenMessengerV2` contract, you can do this with a call to the `approve` function on the USDC contract. If you are interacting with the `CctpExtension` contract on Arbitrum, you sign a `ReceiveWithAuthorization` message. 4. Sign and broadcast a burn transaction. The type depends on the source domain: * **CctpExtension contract on Arbitrum**: Sign and broadcast a `batchDepositForBurnWithAuth` transaction. Set HyperEVM as the destination. Include hook data to call the `CctpForwarder` contract on HyperEVM. * **TokenMessengerV2 contract**: Sign and broadcast a `depositForBurn` transaction. Set HyperEVM as the destination. Include hook data to call the `CctpForwarder` contract on HyperEVM. HyperCore deposits and withdrawals are supported to/from any CCTP-supported blockchain. Forwarding for these transfers is optional and is supported for all [Forwarder-supported blockchains](/cctp/concepts/supported-chains-and-domains#supported-blockchains) except for withdrawals to Solana. ## Important considerations Keep these things in mind when using CCTP with HyperCore. ### Testnet recipient address limitations When you test USDC transfers to HyperCore on testnet, the recipient address has limits: * The recipient address must already exist on HyperCore mainnet. * Addresses that already exist on mainnet can only receive up to \$1000 testnet USDC. * Transfers to addresses without mainnet state fail silently. To check if an address exists on mainnet, use Hyperliquid's info API: ```shell theme={null} curl -X POST https://api.hyperliquid.xyz/info \ -H "Content-Type: application/json" \ -d ' { "type": "userRole", "user": "${USER_ADDRESS}" } ' ``` ### Account activation fee on HyperCore New HyperCore accounts are subject to a one-time 1 USDC activation fee, managed entirely by the Hyperliquid protocol. Circle's `CoreDepositWallet` contract does not collect or enforce this fee. When a new user first deposits USDC to HyperCore, the full deposit amount is credited to their tradable balance. The 1 USDC activation fee is earmarked at the account level and charged on the user's first outbound action, such as a withdrawal or `SendAsset` transfer. Until that first outbound action, the account is considered unactivated and cannot perform [CoreWriter actions](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/activation-gas-fee). This means: * There is no minimum deposit amount. Deposits of any size, including less than 1 USDC, will succeed. * The user's first outbound action (withdrawal, transfer, etc.) requires a balance of at least 1 USDC. If the balance is below 1 USDC at that time, the action will fail. * After the activation fee is paid, subsequent outbound actions are not subject to it. The activation fee is separate from CCTP forwarding fees. When calculating total costs for a new user's first withdrawal, account for both the 1 USDC activation fee (charged by Hyperliquid) and any applicable CCTP forwarding fee. ### Best practices for new account deposits When your integration deposits USDC to a new HyperCore account: * Ensure the deposit is large enough that the recipient will have at least 1 USDC available for their first outbound action. * Inform end users that their first withdrawal or transfer from HyperCore includes a one-time 1 USDC activation fee deducted by the Hyperliquid protocol. * If your integration creates accounts programmatically (for example, contract addresses), you can pre-activate the account by sending an activation transaction to the EVM contract address on HyperCore, as described in [Hyperliquid's documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/activation-gas-fee). # Fast Transfer Allowance Source: https://developers.circle.com/cctp/concepts/fast-transfer-allowance Understanding Circle's Fast Transfer allowance mechanism The Fast Transfer allowance is Circle's mechanism for providing faster-than-finality USDC transfers. It limits the total value of USDC that can be minted through Fast Transfer before related burns reach hard finality. ## How it works Circle maintains a Fast Transfer allowance pool that backs all in-process CCTP Fast Transfers. The following steps describe how the allowance works: 1. **Initial state**: Circle maintains a Fast Transfer allowance pool (for example, 10 million USDC). 2. **Fast Transfer initiated**: When you burn USDC on the source blockchain with Fast Transfer: * The burn amount temporarily debits the allowance * Circle's Attestation Service issues an attestation after [soft finality](/cctp/concepts/finality-and-block-confirmations) * You can immediately mint USDC on the destination blockchain 3. **Allowance depleted**: If the allowance reaches zero, Fast Transfers are temporarily unavailable until the allowance replenishes. 4. **Allowance replenished**: Once burns reach hard finality on source blockchains, the corresponding amounts are credited back to the allowance. **Note:** The Fast Transfer allowance is global across all supported blockchains. It's not specific to a particular source or destination blockchain, but rather tracks the total value of in-process Fast Transfers. ## Check the current allowance To check the remaining Fast Transfer allowance, call the [`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance) endpoint. For a detailed guide on checking the allowance, see [Get the Fast Transfer allowance](/cctp/howtos/get-fast-transfer-allowance). ## When allowance is insufficient If the Fast Transfer allowance is insufficient for your transfer, you have two options: ### Option 1: Wait for replenishment The allowance automatically replenishes as pending Fast Transfers reach hard finality. Monitor the allowance until sufficient capacity is available: ```ts TypeScript theme={null} async function waitForAllowance(requiredAmount: bigint, timeoutMs = 1200000) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/fastBurn/USDC/allowance", ); const { allowance } = await response.json(); // Convert USDC string to 6-decimal subunits without float precision loss const [whole, frac = ""] = String(allowance).split("."); const allowanceSubunits = BigInt(whole) * 1_000_000n + BigInt((frac + "000000").slice(0, 6)); if (allowanceSubunits >= requiredAmount) { console.log("Sufficient allowance available"); return true; } console.log(`Current allowance: ${allowance} USDC, waiting...`); await new Promise((resolve) => setTimeout(resolve, 30000)); // Check every 30 seconds } throw new Error("Timeout waiting for allowance replenishment"); } ``` ### Option 2: Use Standard Transfer Change `minFinalityThreshold` to 2000 or higher to use [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times), which doesn't consume the Fast Transfer allowance. ## Allowance lifecycle Understanding what happens during each phase of the allowance lifecycle helps you build more robust applications: The user calls `depositForBurn` with `minFinalityThreshold` ≤ 1000. The transaction confirms on the source blockchain, and the allowance is debited by the burn amount. Circle's Attestation Service issues an attestation, and the attestation becomes available through the API. The user can now mint USDC on the destination blockchain. The burn transaction reaches hard finality on the source blockchain. The allowance is credited back by the burn amount, and capacity is restored for new Fast Transfers. # CCTP Fees Source: https://developers.circle.com/cctp/concepts/fees Understanding CCTP transfer fees for Fast and Standard transfers CCTP charges fees on Fast Transfers only. Standard Transfers are free. [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) enables USDC transfers at faster-than-finality speeds by leveraging Circle's [Fast Transfer allowance](/cctp/concepts/fast-transfer-allowance). These transfers incur a fee that varies by route. * **Fee range**: 0-14 basis points depending on source blockchain, for example \$0–\$1.40 per \$1,000 transferred * **When and how the fee is collected**: The fee is deducted from the transferred amount when USDC is minted on the destination blockchain ## Get the current fee To retrieve the current Fast Transfer fee for your route, call the [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) endpoint. For more details, see [Get the fee for your transfer](/cctp/howtos/get-transfer-fee). ## Maximum fee parameter When calling [`depositForBurn`](/cctp/references/contract-interfaces#depositforburn), you specify a `maxFee` parameter that sets the maximum fee you're willing to pay: ```ts TypeScript theme={null} await tokenMessenger.depositForBurn( amount, destinationDomain, mintRecipient, burnToken, destinationCaller, 500n, // maxFee: 500 subunits (0.0005 USDC) 1000, // minFinalityThreshold: Fast Transfer ); ``` If the actual fee exceeds your specified `maxFee`, the transaction will revert on the source blockchain, and no USDC will be burned. To avoid transaction failures: 1. Retrieve the current fee before initiating a transfer 2. Add a small buffer (for example, 10-20%) to account for potential fee fluctuations 3. Set `maxFee` to this buffered amount Example: ```ts TypeScript theme={null} async function calculateMaxFee( sourceDomain: number, destDomain: number, transferAmountUSDC: string, // USDC amount like "1" or "10.5" ) { // Convert USDC to subunits (6 decimals) const [whole, decimal = ""] = transferAmountUSDC.split("."); const decimal6 = (decimal + "000000").slice(0, 6); const transferAmount = BigInt(whole + decimal6); // Get current fee const response = await fetch( `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${sourceDomain}/${destDomain}`, ); const fees = await response.json(); // Extract minimumFee for Fast Transfer (finalityThreshold 1000) const minimumFee = fees[0].minimumFee; // Fee in basis points // Calculate fee as percentage of transfer amount const protocolFee = (transferAmount * BigInt(Math.round(minimumFee * 100))) / 1_000_000n; // Add 20% buffer to protocol fee (protocolFee × 1.2) - result in subunits const maxFee = (protocolFee * 120n) / 100n; return maxFee; // denominated in USDC subunits (6 decimals) } // Use in your burn call const maxFee = await calculateMaxFee(0, 1, "10.5"); ``` ## Fee tables The following tables show the current fee rates by source blockchain for Fast and Standard Transfers. Fees are subject to change at any time. **Do not hardcode fee values.** Fees can change at any time. Always retrieve the current fee by calling the [fee API](/api-reference/cctp/all/get-burn-usdc-fees) at least once per week. Hardcoding fees can cause: * **Insufficient fees**: If fees increase, your Fast Transfers may be degraded to Standard Transfers when the provided `maxFee` is below the required threshold. * **Overstated fees**: If fees decrease, users may see higher fees than necessary in your UI, even though the excess is refunded during minting. | Source blockchain | Fee | | ----------------- | ---------------- | | Arbitrum | 1.3 bps (0.013%) | | Base | 1.3 bps (0.013%) | | Codex | 1.5 bps (0.015%) | | EDGE | 1.5 bps (0.015%) | | Ethereum | 1 bps (0.01%) | | Ink | 2 bps (0.02%) | | Linea | 11 bps (0.11%) | | Morph | 4 bps (0.04%) | | OP Mainnet | 1.3 bps (0.013%) | | Plume | 2 bps (0.02%) | | Solana | 1 bps (0.01%) | | Starknet | 14 bps (0.14%) | | Unichain | 1.5 bps (0.015%) | | World Chain | 1.3 bps (0.013%) | **Blockchains without Fast Transfer fees** Some blockchains don't appear in the Fast Transfer fee table because their standard attestation times are already fast enough. Consequently, Fast Transfer is not applicable when these blockchains are used as the source blockchain for burns. For affected blockchains, see [CCTP supported blockchains](/cctp/concepts/supported-chains-and-domains). | Source blockchain | Fee | | ----------------- | ---------- | | Arbitrum | 0 bps (0%) | | Arc Testnet | 0 bps (0%) | | Avalanche | 0 bps (0%) | | Base | 0 bps (0%) | | Codex | 0 bps (0%) | | EDGE | 0 bps (0%) | | Ethereum | 0 bps (0%) | | HyperEVM | 0 bps (0%) | | Injective | 0 bps (0%) | | Ink | 0 bps (0%) | | Linea | 0 bps (0%) | | Monad | 0 bps (0%) | | Morph | 0 bps (0%) | | OP Mainnet | 0 bps (0%) | | Pharos | 0 bps (0%) | | Plume | 0 bps (0%) | | Polygon PoS | 0 bps (0%) | | Sei | 0 bps (0%) | | Solana | 0 bps (0%) | | Sonic | 0 bps (0%) | | Starknet | 0 bps (0%) | | Unichain | 0 bps (0%) | | World Chain | 0 bps (0%) | | XDC | 0 bps (0%) | ## Standard Transfer fee switch Some blockchains support a Standard Transfer fee switch, which enables enforcing a minimum fee during a CCTP Standard Transfer. * Some deployments of the `TokenMessengerV2` contract include a fee switch that enforces a minimum onchain fee. This fee is collected during USDC minting in a Standard Transfer. See tables below for supported blockchains. * `TokenMessengerV2` contracts with fee switch support include the `getMinFeeAmount` function, which calculates and returns the minimum fee required for a given burn amount, in units of the `burnToken`. **Important:** Calling `getMinFeeAmount` on a blockchain that uses an older `TokenMessengerV2` contract (without fee switch support) results in an error. Refer to the tables below to determine which contract version is deployed on each EVM blockchain. ### `TokenMessenger` contracts without fee switch support | Source blockchain | Contract source code | | ----------------- | --------------------------------------------------------------------------------------------------------------------- | | Arbitrum | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Avalanche | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Base | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Codex | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Ethereum | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Linea | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | OP Mainnet | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Polygon PoS | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Sonic | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | Unichain | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | | World Chain | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) | ### `TokenMessenger` contracts with fee switch support | Source blockchain | Contract source code | | ----------------- | ------------------------------------------------------------------------------------------------------------ | | Sei | [`2f9a2ba`](https://github.com/circlefin/evm-cctp-contracts/commit/2f9a2ba993b96a442c75bf21b3cb6d6292d81439) | ## Fee optimization strategies To minimize fees while maximizing transfer speed: * **Choose the right method**: Use Fast Transfer when speed is critical and Standard Transfer when cost optimization is the priority. * **Monitor allowance**: For high-volume applications, monitor the [Fast Transfer allowance](/cctp/concepts/fast-transfer-allowance) and switch to Standard Transfer when it's low. * **Batch transfers**: If you're making multiple transfers, consider batching them during periods when Fast Transfer allowance is high. * **Set appropriate `maxFee`**: Always retrieve the current fee before initiating a transfer and set `maxFee` with a buffer to account for minor fluctuations. # Finality and Block Confirmations Source: https://developers.circle.com/cctp/concepts/finality-and-block-confirmations Block confirmation requirements and attestation timing for CCTP Before signing an attestation, Circle waits for blockchain transactions to achieve the appropriate level of transaction finality. The required finality level depends on whether you use Fast Transfer or Standard Transfer. * Fast Transfer: Attestations are issued after the transaction is confirmed and included in a block, typically in seconds. Because of the faster finality time, Fast Transfers are subject to a global allowance to mitigate reorganization risks. * Standard Transfer: Attestations are issued after hard finality, when the transaction is unlikely to be reversed by a chain reorganization, typically in minutes. ## Fast Transfer attestation times The table below shows the average time for attestations to become available when using Fast Transfer (`minFinalityThreshold` ≤ 1000): | Source blockchain | Block confirmations | Average time | | ----------------- | ------------------- | ------------ | | **Ethereum** | 2 | \~20 seconds | | **Arbitrum** | 1 | \~8 seconds | | **Base** | 1 | \~8 seconds | | **Codex** | 1 | \~8 seconds | | **EDGE** | 1 | \~8 seconds | | **Ink** | 1 | \~8 seconds | | **Linea** | 1 | \~8 seconds | | **Morph** | 1 | \~8 seconds | | **OP Mainnet** | 1 | \~8 seconds | | **Plume** | 1 | \~8 seconds | | **Solana** | 2-3 | \~8 seconds | | **Starknet** | 4 | \~20 seconds | | **Unichain** | 1 | \~8 seconds | | **World Chain** | 1 | \~8 seconds | **Blockchains without Fast Transfer:** Some blockchains don't support Fast Transfer as a source blockchain because their standard attestation times are already fast. For those [CCTP supported blockchains](/cctp/concepts/supported-chains-and-domains) where Fast Transfer is disabled as a source, use Standard Transfer instead. ## Standard Transfer attestation times The table below shows the average time for attestations to become available when using Standard Transfer (`minFinalityThreshold` ≥ 2000): | Source blockchain | Block confirmations | Average time | | ------------------- | ------------------- | --------------- | | **Ethereum** | \~65 | \~15-19 minutes | | **Arbitrum** | \~65 ETH blocks | \~15-19 minutes | | **Arc Testnet** | 1 | \~0.5 seconds | | **Avalanche** | 1 | \~8 seconds | | **Base** | \~65 ETH blocks | \~15-19 minutes | | **BNB Smart Chain** | 3 | \~2 seconds | | **Codex** | \~65 ETH blocks | \~15-19 minutes | | **EDGE** | \~65 ETH blocks | \~16-21 minutes | | **HyperEVM** | 1 | \~5 seconds | | **Injective** | 1 | \~0.65 seconds | | **Ink** | \~65 ETH blocks | \~30 minutes | | **Linea** | 1 | \~6-32 hours | | **Monad** | 1 | \~5 seconds | | **Morph** | \~65 ETH blocks | \~20-30 minutes | | **OP Mainnet** | \~65 ETH blocks | \~15-19 minutes | | **Pharos** | 1 | \~7 seconds | | **Plume** | \~65 ETH blocks | \~15-19 minutes | | **Polygon PoS** | 2-3 | \~8 seconds | | **Sei** | 1 | \~5 seconds | | **Solana** | 32 | \~25 seconds | | **Sonic** | 1 | \~8 seconds | | **Starknet** | \~65 ETH Blocks | \~4 to 8 hours | | **Stellar** | 1 | \~5 seconds | | **Unichain** | \~65 ETH blocks | \~15-19 minutes | | **World Chain** | \~65 ETH blocks | \~15-19 minutes | | **XDC** | 3 | \~10 seconds | ## Layer 2 finality Layer 2 (L2) blockchains built on Ethereum publish transaction data in batches to Ethereum Layer 1. The finality characteristics of L2 chains depend on when batches are posted and when those batches achieve finality on Ethereum L1. OP Stack-based chains (including Base, OP Mainnet, and World Chain) post state updates using [EIP-4844](https://www.eip4844.com/) blob transactions approximately every 15 minutes. Circle waits for the Ethereum L1 block containing the batch to finalize, which typically takes \~65 blocks (15-19 minutes) after the batch is posted. Linea has a longer finality period compared to other L2 chains. Standard Transfer on Linea typically requires 6-32 hours before attestations become available. The typical time to reach hard finality on Starknet is 4–8 hours, as finality depends on when the zk-rollup proof is posted to Ethereum and when its corresponding L1 block finalizes. ## Solana finality Solana uses a different finality model: Circle waits for the block to be confirmed (votes from validators representing over two-thirds of total stake). This typically takes 2-3 blocks (\~8 seconds). Circle waits for block finality, which takes 32 blocks (\~25 seconds). # Circle Forwarding Service for CCTP Source: https://developers.circle.com/cctp/concepts/forwarding-service Forward destination chain mints to simplify crosschain transfers The Circle Forwarding Service is a service for CCTP that simplifies integration by removing the need for you to run multichain infrastructure. This can improve user experience for crosschain transfers by ensuring reliability and eliminating the need to handle destination chain gas fees. ## How it works A CCTP transfer without the Forwarding Service is a three-step process: 1. Create a transaction to burn USDC on the source chain and wait for Circle to sign an attestation. 2. Request an attestation from the Circle API. 3. Create a transaction to mint USDC on the destination chain. This process requires you to have a wallet that can sign transactions on the source and destination chains, and native tokens for paying the transaction gas fee on both chains. You use the Forwarding Service by including a forward request in the hook data of the burn transaction on the source chain. Circle validates the hook data, signs the attestation, and broadcasts the mint transaction on the destination chain for you, removing the need for you to handle the transaction on the destination chain. For a full example of how to use the Forwarding Service, see [Transfer USDC with the Forwarding Service](/cctp/howtos/transfer-usdc-with-forwarding-service). ### Hook format The hook data for Forwarding Service begins with the reserved magic bytes `cctp-forward` followed by versioning and payload fields. You can append your own custom hook data after Circle's reserved space. Forwarding Service doesn't support forwarding to wrapper contracts (for example, when `destinationCaller` is set). | Bytes | Type | Data | | ----- | --------- | ------------------------------------------------- | | 0-23 | `bytes24` | `cctp-forward` | | 24-27 | `uint32` | Version, set to `0` | | 28-31 | `uint32` | Length of additional Circle hook data, set to `0` | | 32-51 | `any` | Developer-defined hook data | If no additional integrator hook data is required, a static hex string can be used for the forwarding hook data: ```javascript theme={null} // Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0) const forwardHookData = "0x636374702d666f72776172640000000000000000000000000000000000000000"; ``` ### Solana `mintRecipient` When the destination blockchain is Solana, the `mintRecipient` parameter in `depositForBurnWithHook` must be the recipient's USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account) address, not the recipient's wallet address. Unlike EVM destinations where `mintRecipient` is the wallet address, Solana requires the address of the SPL token account that will hold the minted USDC. You can derive the ATA address from the recipient's wallet address and the USDC mint address using the [`getAssociatedTokenAddressSync`](https://solana-labs.github.io/solana-program-library/token/js/functions/getAssociatedTokenAddressSync.html) function from the `@solana/spl-token` library: ```typescript theme={null} import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; const recipientWallet = new PublicKey("RecipientWalletAddress"); const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet); const mintRecipient = `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}`; ``` ### Solana hook data for ATA creation If the recipient does not have an existing USDC ATA, you can request the Forwarding Service to create it by encoding additional fields in the hook data. The extended hook data format is: | Bytes | Type | Data | | ----- | --------- | -------------------------------------------------- | | 0-23 | `bytes24` | `cctp-forward` | | 24-27 | `uint32` | Version, set to `0` | | 28-31 | `uint32` | Length of additional Circle hook data, set to `33` | | 32 | `uint8` | `1` (request ATA creation) | | 33-64 | `bytes32` | Recipient wallet address (ATA owner) | | 65+ | `any` | Developer-defined hook data | When using this format, the `mintRecipient` must be the ATA derived from the wallet address in bytes 33-64 and the USDC mint. The Forwarding Service validates that these values are consistent before creating the account. The following example shows how to construct the extended hook data for Solana with ATA creation: ```typescript theme={null} import { PublicKey } from "@solana/web3.js"; // Magic bytes "cctp-forward" padded to 24 bytes const magicBytes = Buffer.alloc(24); magicBytes.write("cctp-forward", "utf-8"); // Version (uint32, big-endian) = 0 const version = Buffer.alloc(4); // Length of additional Circle hook data (uint32, big-endian) = 33 const length = Buffer.alloc(4); length.writeUInt32BE(33); // ATA creation flag = 1 const ataFlag = Buffer.from([1]); // Recipient wallet address (32 bytes) const recipientWallet = new PublicKey("RecipientWalletAddress"); const walletBytes = Buffer.from(recipientWallet.toBytes()); const forwardHookData = "0x" + Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString( "hex", ); ``` If the recipient already has a USDC ATA and no ATA creation is needed, use the same static hook data as EVM: ```typescript theme={null} // Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0) const forwardHookData = "0x636374702d666f72776172640000000000000000000000000000000000000000"; ``` ## Fees and execution The Forwarding Service charges a fee for each transfer, in addition to the CCTP protocol fee. The Forwarding Service fee charged is to cover gas costs on the destination chain and a small service fee. The Forwarding Service prioritizes fast execution and quotes gas dynamically. If gas used is less than gas needed for execution, the remainder is spent as an additional priority fee where they are supported. On all chains, a higher fee provides a safety buffer for successful transaction delivery on the destination chain. Circle does not refund for excess gas and does not keep the excess gas, except in cases where excess priority fees are rejected. The [`depositForBurnWithHook`](/cctp/references/contract-interfaces#depositforburnwithhook) transaction includes a `maxFee` parameter. When using the Forwarding Service, this parameter should be set to a value that is large enough to cover the CCTP protocol fee and the Forwarding Service fee. Because the gas budget for the destination chain comes from a USDC fee on the source chain, choosing a lower `maxFee` results in a lower priority fee on the destination chain. A higher `maxFee` results in a higher priority fee on the destination chain and can result in faster confirmation. The Forwarding Service charges a service fee for each transfer: | Destination chain | Service fee (USDC) | | ----------------- | ------------------ | | All chains | \$0.20 | If the `maxFee` parameter is insufficient to cover the both Fast Transfer protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding execution over Fast Transfer. This means that the transfer will execute as a Standard Transfer with the Forwarding Service. ### Solana fees When forwarding to Solana, the Forwarding Service fee includes both a gas component and a rent component. Rent covers the cost of creating onchain accounts required by each transfer. The fee estimate API returns `forwardFee` values that already include rent, so no additional calculation is needed on your part. If the recipient does not already have a USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account) on Solana, the transfer will fail. To have the Forwarding Service create the ATA, you must: 1. Pass `includeRecipientSetup=true` when calling the fee estimate API so the returned `forwardFee` covers the ATA creation cost. 2. Encode the ATA creation fields in the [hook data](#solana-hook-data-for-ata-creation) of the burn transaction. ```http theme={null} GET /v2/burn/USDC/fees/{sourceDomain}/{destDomain}?forward=true&includeRecipientSetup=true ``` For full details on this endpoint, see the [`GET /v2/burn/USDC/fees` API reference](/api-reference/cctp/all/get-burn-usdc-fees). `includeRecipientSetup` only applies when the destination blockchain is Solana. It has no effect for EVM destination blockchains. ## Supported blockchains For a full list of supported blockchains, see [CCTP Supported Blockchains](/cctp/concepts/supported-chains-and-domains). # Supported Blockchains and Domains Source: https://developers.circle.com/cctp/concepts/supported-chains-and-domains Blockchains and domain identifiers supported by CCTP CCTP is available on multiple blockchains where USDC is natively issued. Each blockchain is assigned a unique domain identifier used in [CCTP contracts](/cctp/references/contract-addresses) and API calls. ## Supported blockchains CCTP provides [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times), [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times), Hooks, and [Forwarding Service](/cctp/concepts/forwarding-service) capabilities on the following blockchains. All chains listed below are supported as destination chains. | Blockchain | Source (Standard transfer) | Source (Fast transfer) | Forwarding Service | | --------------------------- | -------------------------- | ---------------------- | ------------------ | | Arc Testnet | ✅ | ❌ | ✅ | | Arbitrum | ✅ | ✅ | ✅ | | Avalanche | ✅ | ❌ | ✅ | | Base | ✅ | ✅ | ✅ | | BNB Smart Chain (USYC only) | ✅ | ✅ | ❌ | | Codex | ✅ | ✅ | ✅ | | EDGE | ✅ | ✅ | ✅ | | Ethereum | ✅ | ✅ | ✅ | | HyperEVM | ✅ | ❌ | ✅ | | Injective | ✅ | ❌ | ❌ | | Ink | ✅ | ✅ | ✅ | | Linea | ✅ | ✅ | ✅ | | Monad | ✅ | ❌ | ✅ | | Morph | ✅ | ✅ | ❌ | | OP Mainnet | ✅ | ✅ | ✅ | | Pharos | ✅ | ❌ | ❌ | | Plume | ✅ | ✅ | ✅ | | Polygon PoS | ✅ | ❌ | ✅ | | Sei | ✅ | ❌ | ✅ | | Solana | ✅ | ✅ | ✅ | | Sonic | ✅ | ❌ | ✅ | | Starknet | ✅ | ✅ | ❌ | | Stellar | ✅ | ✅ | ❌ | | Unichain | ✅ | ✅ | ✅ | | World Chain | ✅ | ✅ | ✅ | | XDC | ✅ | ❌ | ✅ | On Stellar, USDC precision and address encoding differ from other CCTP-supported blockchains. For inbound transfers, use [`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) so funds reach the correct recipient. See [CCTP on Stellar](/cctp/references/stellar). **Forwarding Service support:** The column labeled "Forwarding Service" indicates whether the blockchain is available as a destination chain for the [Circle Forwarding Service](/cctp/concepts/forwarding-service). **Testnet support:** If a mainnet is listed, its official testnet is also supported. For example, Ethereum includes both Ethereum Mainnet and Ethereum Sepolia. **Fast Transfer availability:** [Fast Transfer](/cctp/concepts/fast-transfer-allowance) is available for source chains only when it provides a meaningful speed improvement over standard burn attestation times. For blockchains where standard attestation is already fast, Fast Transfer does not provide additional value. ## Domain identifiers A domain is a Circle-issued identifier for a blockchain where CCTP contracts are deployed. Domain identifiers don't map to existing public chain IDs. Use domain identifiers when calling CCTP contracts and API endpoints: | Domain | Blockchain | | :----- | :-------------- | | 0 | Ethereum | | 1 | Avalanche | | 2 | OP Mainnet | | 3 | Arbitrum | | 5 | Solana | | 6 | Base | | 7 | Polygon PoS | | 10 | Unichain | | 11 | Linea | | 12 | Codex | | 13 | Sonic | | 14 | World Chain | | 15 | Monad | | 16 | Sei | | 17 | BNB Smart Chain | | 18 | XDC | | 19 | HyperEVM | | 21 | Ink | | 22 | Plume | | 25 | Starknet | | 26 | Arc Testnet | | 27 | Stellar | | 28 | EDGE | | 29 | Injective | | 30 | Morph | | 31 | Pharos | ## Supported tokens Not all domains support the same tokens: * [USDC](/stablecoins/what-is-usdc): Supported on all CCTP domains except BNB Smart Chain * [USYC](/tokenized/usyc/overview): Supported only on Ethereum and BNB Smart Chain ## CCTP V1 (Legacy) only The following blockchains are supported only by CCTP V1 (Legacy). If you are building on these chains, refer to the [V1 documentation](/cctp/v1) for integration guides and contract references. | Blockchain | Domain | Documentation | | ---------- | ------ | ------------------------------------------------------------------------------------------------------------- | | Aptos | 9 | [Aptos packages](/cctp/v1/aptos-packages), [Quickstart](/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base) | | Noble | 4 | [Noble Cosmos module](/cctp/v1/noble-cosmos-module) | | Sui | 8 | [Sui packages](/cctp/v1/sui-packages), [Quickstart](/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum) | # Get the Fast Transfer Allowance Source: https://developers.circle.com/cctp/howtos/get-fast-transfer-allowance Check the remaining Fast Transfer allowance for USDC transfers This how-to shows you how to retrieve the remaining Fast Transfer allowance using the [CCTP API](/api-reference/cctp/all/get-fast-burn-usdc-allowance). The Fast Transfer allowance is Circle's mechanism for backing faster-than-finality USDC transfers before burns reach hard finality on source chains. ## Prerequisites Before you begin, ensure you have: * Installed cURL on your development machine ## Get the Fast Transfer allowance Call the [`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance) endpoint to retrieve the current remaining Fast Transfer allowance. **Example request** ```shell Shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/fastBurn/USDC/allowance' \ --header 'Accept: application/json' ``` **Response** ```json theme={null} { "allowance": 99999999225.24174, "lastUpdated": "2025-12-02T13:17:02.453Z" } ``` The response includes: * `allowance`: The remaining Fast Transfer allowance in USDC units * `lastUpdated`: The UTC timestamp when the allowance was last updated **Fast Transfer allowance details:** * The allowance represents the total value of USDC that can be minted through Fast Transfer before related burns on source chains reach hard finality. * When you initiate a Fast Transfer, the burn amount temporarily debits the allowance. * Once the burn reaches finality on the source chain, the corresponding amount is credited back to the allowance. * If the allowance is insufficient for your transfer, you should either wait for the allowance to replenish or use Standard Transfer instead. # Get the Fee for Your Transfer Source: https://developers.circle.com/cctp/howtos/get-transfer-fee Retrieve CCTP transfer fees using the API This guide shows you how to retrieve the fee for a USDC transfer using the CCTP API. Fees vary based on the source and destination blockchains, and whether you use Fast Transfer or Standard Transfer. ## Prerequisites Before you begin, ensure you have: * Installed cURL on your development machine ## Get the transfer fee Call the [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) endpoint to retrieve the fees for transferring USDC between two blockchains. **Request parameters** * `sourceDomainId`: The [domain ID](/cctp/concepts/supported-chains-and-domains#domain-identifiers) of the source blockchain * `destDomainId`: The domain ID of the destination blockchain **Example request** ```shell Shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/0/26' \ --header 'Accept: application/json' ``` This example retrieves the fees for transferring USDC from Ethereum Sepolia (domain 0) to Arc Testnet (domain 26). **Response** ```json theme={null} [ { "finalityThreshold": 1000, "minimumFee": 1 }, { "finalityThreshold": 2000, "minimumFee": 0 } ] ``` **Fee details:** * Fees are specified in basis points (bps), for example, 1 = 0.01%. * Fast Transfer fees vary by route. * You specify the maximum fee you're willing to pay when calling `depositForBurn`. The actual fee charged will not exceed this amount. # Resolve Attestation Issues Source: https://developers.circle.com/cctp/howtos/resolve-stuck-attestation Troubleshoot and resolve common problems with CCTP attestations This guide helps you resolve issues when a CCTP attestation takes longer than expected to become available or when the attestation API returns unexpected responses. ## Understanding attestation timing After a successful burn transaction, Circle's Attestation Service (Iris) must: 1. Observe the burn event on the source blockchain 2. Wait for sufficient block confirmations 3. Sign the message and make the attestation available through the [CCTP API](/api-reference/cctp/all/get-messages-v2) This process takes different amounts of time depending on the transfer type: | Transfer type | Finality threshold | Typical wait time | | ----------------- | ------------------ | -------------------------------------------------------------------------------------------------------------- | | Fast Transfer | ≤ 1000 | Seconds to a few minutes | | Standard Transfer | ≥ 2000 | Varies by blockchain (see [Finality and Block Confirmations](/cctp/concepts/finality-and-block-confirmations)) | ## Why 404 responses are expected The attestation API returns a 404 response until the attestation service has observed and processed your burn transaction. This is expected and does not indicate an error. The API returns 404 when: * The burn transaction hasn't reached the required block confirmations * The attestation service hasn't yet indexed the transaction * The transaction hash or domain ID is incorrect Don't treat 404 as a failure. Instead, implement polling with appropriate intervals. ## Check attestation status Query the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint to check the current status of your attestation. The following table explains the possible responses and what to do next: | Response | Meaning | Action | | -------------------------- | ----------------------------------- | ---------------------------------------------------------- | | 404 | Attestation not yet observed | Continue polling until the attestation is available | | `{ "messages": [] }` | Transaction found but not processed | Continue polling until the transaction is processed | | `{ "status": "pending" }` | Awaiting block confirmations | Continue polling until the block confirmations are reached | | `{ "status": "complete" }` | Attestation ready | Proceed to mint | ## Implement effective polling Poll the attestation API at regular intervals without exceeding rate limits: ```ts TypeScript theme={null} async function waitForAttestation( sourceDomain: number, transactionHash: string, ) { const pollInterval = 5000; // Poll every 5 seconds const maxWaitTime = 1200000; // 20 minutes maximum const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { try { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${sourceDomain}?transactionHash=${transactionHash}`, ); // 404 is expected while waiting - continue polling if (response.status === 404) { console.log("Attestation not yet available (404), waiting..."); await new Promise((resolve) => setTimeout(resolve, pollInterval)); continue; } // Rate limited - wait before retrying if (response.status === 429) { console.log("Rate limited, waiting 5 minutes..."); await new Promise((resolve) => setTimeout(resolve, 300000)); continue; } if (!response.ok) { throw new Error(`Unexpected HTTP error: ${response.status}`); } const data = await response.json(); // Empty messages array - transaction found but not processed if (!data.messages || data.messages.length === 0) { console.log("Transaction found, awaiting processing..."); await new Promise((resolve) => setTimeout(resolve, pollInterval)); continue; } const message = data.messages[0]; // Attestation complete if (message.status === "complete" && message.attestation) { console.log("Attestation retrieved successfully!"); return { message: message.message, attestation: message.attestation, decodedMessage: message.decodedMessage, }; } // Still pending console.log(`Attestation status: ${message.status}`); await new Promise((resolve) => setTimeout(resolve, pollInterval)); } catch (error) { console.error("Error fetching attestation:", (error as Error).message); await new Promise((resolve) => setTimeout(resolve, pollInterval)); } } throw new Error("Attestation not received within maximum wait time"); } ``` ### Avoid rate limiting The attestation service limits requests to 35 per second. If you exceed this limit, the service blocks all API requests for 5 minutes and returns HTTP 429. Best practices: * Use a poll interval of at least 5 seconds * Implement exponential back-off for repeated 429 responses * Don't poll from multiple clients for the same transaction ## Troubleshooting checklist If your attestation isn't available after the expected wait time: Check the source blockchain's block explorer to confirm: * The transaction succeeded (not reverted) * The transaction is included in a mined block * Sufficient blocks have been confirmed since the transaction Verify you're using the correct: * Source domain ID (see [Supported Chains and Domains](/cctp/concepts/supported-chains-and-domains)) * Transaction hash (full hash, including `0x` prefix for EVM chains) * API environment (sandbox vs. production) Standard Transfers require full finality. For some blockchains, this can take significantly longer than Fast Transfers. Check [Finality and Block Confirmations](/cctp/concepts/finality-and-block-confirmations) for expected times. Test that you can reach the API: ```shell Shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/publicKeys' \ --header 'Accept: application/json' ``` If this fails, check your network connectivity and firewall settings. # Retry a Failed Mint Source: https://developers.circle.com/cctp/howtos/retry-failed-mint Complete a CCTP transfer when the mint transaction fails This guide helps you complete a CCTP transfer when you have a valid attestation but the mint transaction on the destination blockchain fails or was never submitted. ## Minting is safe to retry CCTP minting is idempotent. Each attestation contains a unique nonce that can only be used once. If you submit the same attestation multiple times, only the first successful transaction mints USDC. Subsequent attempts revert with a "nonce already used" error but don't result in duplicate minting. This means you can safely retry a failed mint without risking double-spending. ## Common mint failure reasons | Failure reason | Symptoms | Solution | | ------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------- | | Insufficient gas | Transaction reverts or times out | Increase gas limit and retry | | Nonce already used | Transaction reverts with nonce error | The mint already succeeded; check recipient balance | | Wrong contract address | Transaction may succeed with no USDC minted | Verify you're using the correct `MessageTransmitterV2` address for the destination blockchain | | Destination caller restriction | Transaction reverts | Check if the burn specified a `destinationCaller`; only that address can mint | | Token account doesn't exist (Solana) | Transaction fails | Create the recipient's USDC token account first | | Attestation expired | Transaction reverts | Use re-attestation API to get a fresh attestation | ## Verify the current state Before retrying, check whether the mint already succeeded: Query the recipient's USDC balance on the destination blockchain. If the expected amount is present, the mint already completed. Search the destination blockchain's block explorer for `receiveMessage` transactions from your wallet to the `MessageTransmitterV2` contract. Query the [attestation API](/api-reference/cctp/all/get-messages-v2) to confirm you have a `complete` status: ## Retry the mint transaction If the mint hasn't completed, submit a new `receiveMessage` transaction using your attestation. Call `receiveMessage` on the `MessageTransmitterV2` contract: ```ts TypeScript theme={null} import { createWalletClient, createPublicClient, http, encodeFunctionData, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationData { message: string; attestation: string; } const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); const walletClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); // MessageTransmitterV2 contract address - verify for your destination chain // See: https://developers.circle.com/cctp/references/contract-addresses const MESSAGE_TRANSMITTER_V2 = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; async function retryMint(data: AttestationData) { console.log("Retrying mint transaction..."); try { const txHash = await walletClient.sendTransaction({ to: MESSAGE_TRANSMITTER_V2, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ data.message as `0x${string}`, data.attestation as `0x${string}`, ], }), }); console.log(`Mint transaction submitted: ${txHash}`); // Wait for confirmation const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); if (receipt.status === "success") { console.log("Mint successful!"); return { success: true, txHash }; } else { console.log("Mint transaction reverted"); return { success: false, txHash }; } } catch (error) { // Check if the error indicates nonce already used const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("nonce") || errorMessage.includes("already")) { console.log( "Nonce already used - mint may have already completed. Check recipient balance.", ); } throw error; } } // Use attestation data from the API const attestationData: AttestationData = { message: "0x00000001000000000000001a...", // Full message hex from API attestation: "0xde09db65dea64090570d8143...", // Full attestation hex from API }; await retryMint(attestationData); ``` Call `receiveMessage` on the `MessageTransmitterV2` program. For Solana, ensure the recipient's USDC token account exists before calling `receiveMessage`. ```ts TypeScript theme={null} import crypto from "crypto"; import { address, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, getAddressEncoder, getProgramDerivedAddress, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, signTransactionMessageWithSigners, } from "@solana/kit"; import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system"; import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; interface AttestationData { message: string; attestation: string; } // Solana Configuration const SOLANA_RPC = "https://api.devnet.solana.com"; const SOLANA_WS = "wss://api.devnet.solana.com"; const rpc = createSolanaRpc(SOLANA_RPC); const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS); const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!); const solanaKeypair = await createKeyPairSignerFromBytes( Uint8Array.from(solanaPrivateKey), ); // Solana CCTP Program Addresses (Devnet) const MESSAGE_TRANSMITTER_PROGRAM = address( "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", ); const TOKEN_MESSENGER_MINTER_PROGRAM = address( "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", ); const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); const ASSOCIATED_TOKEN_PROGRAM = address( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", ); async function retryMintOnSolana( attestationData: AttestationData, sourceDomain: number, ) { console.log("Retrying mint on Solana..."); const addressEncoder = getAddressEncoder(); // Derive receiver's USDC token account const [receiverUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); // Derive required PDAs const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [authorityPda] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter_authority")], }); // Calculate used nonces PDA const messageBytes = Buffer.from(attestationData.message.slice(2), "hex"); const nonce = messageBytes.readBigUInt64BE(12); const firstNonce = (nonce / 6400n) * 6400n; const firstNonceBuffer = Buffer.alloc(8); firstNonceBuffer.writeBigUInt64BE(firstNonce); const sourceDomainBuffer = Buffer.alloc(4); sourceDomainBuffer.writeUInt32BE(sourceDomain); const [usedNonces] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [ new TextEncoder().encode("used_nonces"), sourceDomainBuffer, firstNonceBuffer, ], }); // Derive TokenMessengerMinterV2 PDAs const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(sourceDomain.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); const sourceTokenBytes = messageBytes.slice(133, 165); const [tokenPair] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("token_pair"), sourceDomainBuffer, sourceTokenBytes, ], }); const [custody] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("custody"), addressEncoder.encode(USDC_MINT), ], }); const [eventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [tokenProgramEventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); // Build instruction const discriminator = crypto .createHash("sha256") .update("global:receive_message") .digest() .slice(0, 8); const messageBuffer = Buffer.from(attestationData.message.slice(2), "hex"); const attestationBuffer = Buffer.from( attestationData.attestation.slice(2), "hex", ); const messageLenBuffer = Buffer.alloc(4); messageLenBuffer.writeUInt32LE(messageBuffer.length); const attestationLenBuffer = Buffer.alloc(4); attestationLenBuffer.writeUInt32LE(attestationBuffer.length); const instructionData = new Uint8Array( Buffer.concat([ discriminator, messageLenBuffer, messageBuffer, attestationLenBuffer, attestationBuffer, ]), ); const receiveMessageIx = { programAddress: MESSAGE_TRANSMITTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 0 }, { address: authorityPda, role: 0 }, { address: messageTransmitter, role: 0 }, { address: usedNonces, role: 1 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: eventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: tokenMessenger, role: 0 }, { address: remoteTokenMessenger, role: 0 }, { address: tokenMinter, role: 1 }, { address: localToken, role: 1 }, { address: tokenPair, role: 0 }, { address: receiverUsdcAccount, role: 1 }, { address: custody, role: 1 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: tokenProgramEventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(receiveMessageIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); try { await sendAndConfirmTransaction( signedTransaction as Parameters[0], { commitment: "confirmed", }, ); const signature = getSignatureFromTransaction(signedTransaction); console.log(`Mint successful! Signature: ${signature}`); return signature; } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("already been processed")) { console.log("Nonce already used - mint may have already completed."); } throw error; } } // Use attestation data from the API const attestationData: AttestationData = { message: "0x000000000000000500000000...", // Full message hex from API attestation: "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d43...", // Full attestation hex from API }; await retryMintOnSolana(attestationData, 0); // 0 = Ethereum Sepolia domain ``` **Note:** The recipient's USDC token account must exist before calling `receiveMessage`. If the account doesn't exist, create it using the Associated Token Program before retrying the mint. ## Handle destination caller restrictions If the burn specified a `destinationCaller` address, only that address can call `receiveMessage`. If you're seeing authorization errors: 1. Check the `destinationCaller` field in the attestation's `decodedMessage` 2. If it's not `0x0000...0000`, ensure you're calling from the specified address # Transfer USDC from Arbitrum to HyperCore Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore This guide shows how to transfer USDC from Arbitrum to HyperCore using the `CctpExtension` contract. Fast Transfers from Arbitrum to HyperEVM have no fees, however there is a flat forwarding fee for Arbitrum transfers to HyperCore. Fast Transfer is the default for transfers from Arbitrum to HyperEVM. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM testnet wallet with the private key available * Added Arbitrum Sepolia network to your wallet ([network details](https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers)) * Funded your wallet with the following testnet tokens: * Arbitrum Sepolia ETH (native token) from a [public faucet](https://faucet.quicknode.com/arbitrum/sepolia) * Arbitrum Sepolia USDC from the [Circle Faucet](https://faucet.circle.com) * Created a new Node project and installed dependencies: ```bash theme={null} npm install viem npm install -D tsx typescript @types/node ``` * Created a `.env` file with required environment variables: ```text theme={null} PRIVATE_KEY=0x... FORWARD_RECIPIENT=0x... # Your HyperCore address to receive the USDC ``` ## Steps Use the following steps to transfer USDC from Arbitrum to HyperCore. ### Step 1. Get CCTP fees from the API Query the CCTP API for the fees for transferring USDC from Arbitrum to HyperCore. This value is passed to the `maxFee` parameter in the `batchDepositForBurnWithAuth` transaction. The following is an example request to the CCTP using source domain 3 (Arbitrum) and destination domain 19 (HyperEVM): ```shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/3/19?forward=true&hyperCoreDeposit=true' \ --header 'Content-Type: application/json' ``` **Response:** ```json theme={null} [ { "finalityThreshold": 1000, // fast transfer "minimumFee": 0, // no protocol fee "forwardFee": { "low": 200000, // 0.20 USDC "med": 200000, // low, med, high will be the same static fee "high": 200000 } }, { "finalityThreshold": 2000, // standard transfer "minimumFee": 0, "forwardFee": { "low": 200000, "med": 200000, "high": 200000 } } ] ``` ### Step 2. Calculate the USDC amounts minus fees There is no fee to deposit USDC from Arbitrum to HyperEVM, but there is a flat forwarding fee for the transfer to HyperCore. The forwarding fee is 0.20 USDC (`0_200_000` subunits). For a 10 USDC transfer from Arbitrum to HyperCore, the total fee is 0.20 USDC. The forwarding fee is deducted from your transfer amount. For a 10 USDC transfer, you will receive 9.80 USDC on HyperCore. ### Step 3. Sign a `ReceiveWithAuthorization` transaction on the USDC contract Create a `ReceiveWithAuthorization` transaction for the USDC contract with the following parameters: * `from`: Your wallet address * `to`: The `CctpExtension` contract address * `value`: The amount of USDC to transfer * `validAfter`: The timestamp after which the transaction is valid * `validBefore`: The timestamp before which the transaction is valid * `nonce`: A random nonce Sign the hash of the transaction with your private key, and derive the `v`, `r`, `s` values. Broadcast the transaction to the blockchain. ### Step 4. Sign and broadcast a `batchDepositForBurnWithAuth` transaction on the `CctpExtension` contract Create a `batchDepositForBurnWithAuth` transaction for the `CctpExtension` contract with the following parameters: * `destinationDomain`: 19 (HyperEVM) * `mintRecipient`: The `CctpForwarder` contract address on HyperEVM * `destinationCaller`: The `CctpForwarder` contract address on HyperEVM * `maxFee`: `0_200_000` (0.20 USDC, from step 2) * `minFinalityThreshold`: `1000` (Fast Transfer) * `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM when you transfer USDC to HyperCore. * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. The `hookData` is the data to execute the forwarder to HyperCore. The following is an example of the hook data: ```ts TypeScript theme={null} /** * Generate CCTP forwarder hook data for HyperCore * * Hook Data Format: * Field Bytes Type Index * magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding * version 4 uint32 24 * dataLength 4 uint32 28 * hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore * hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot) */ function encodeForwardHookData( hyperCoreMintRecipient?: `0x${string}`, hyperCoreDestinationDex: number = 0, ): `0x${string}` { // Validate hex prefix if recipient provided if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) { throw new Error("Address must start with 0x"); } // Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros const magic = "cctp-forward"; const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0"); // Version: uint32 = 0 (4 bytes, big-endian) const version = "00000000"; if (!hyperCoreMintRecipient) { // No recipient: dataLength = 0, return header only (32 bytes) const dataLength = "00000000"; return `0x${magicHex}${version}${dataLength}`; } // With recipient: dataLength = 24 (20 bytes address + 4 bytes dex) const dataLength = "00000018"; // 24 in hex // Address: 20 bytes (remove 0x prefix) const address = hyperCoreMintRecipient.slice(2).toLowerCase(); // Destination DEX: uint32 big-endian // 0 = perps, 4294967295 (0xFFFFFFFF) = spot const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0"); return `0x${magicHex}${version}${dataLength}${address}${dex}`; } ``` Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and automatically forwarded to your address on HyperCore. By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps balance on HyperCore. To deposit to the spot balance, set `hyperCoreDestinationDex` to `4294967295` (uint32 max value). ## Full example code The following is a complete example of how to transfer USDC from Arbitrum to HyperCore. ```ts script.ts expandable theme={null} /** * Script: Call CctpExtension.batchDepositForBurnWithAuth * - Generates EIP-3009 receiveWithAuthorization signature * - Executes a CCTP burn via the extension * - Supports Forwarder hook data to auto-forward to HyperCore */ import { createWalletClient, createPublicClient, http, parseUnits, formatUnits, type Address, type Hex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arbitrumSepolia } from "viem/chains"; // -------- Contract ABIs -------- const CCTP_EXTENSION_ABI = [ { name: "batchDepositForBurnWithAuth", type: "function", stateMutability: "nonpayable", inputs: [ { name: "_receiveWithAuthorizationData", type: "tuple", components: [ { name: "amount", type: "uint256" }, { name: "authValidAfter", type: "uint256" }, { name: "authValidBefore", type: "uint256" }, { name: "authNonce", type: "bytes32" }, { name: "v", type: "uint8" }, { name: "r", type: "bytes32" }, { name: "s", type: "bytes32" }, ], }, { name: "_depositForBurnData", type: "tuple", components: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], }, ], outputs: [], }, ] as const; // -------- Configuration -------- const config = { privateKey: (process.env.PRIVATE_KEY || "0x") as Hex, // Contract addresses (Arbitrum Sepolia Testnet) cctpExtension: "0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D" as Address, usdcToken: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d" as Address, // Transfer parameters amount: "2", // USDC amount to transfer maxFee: "0.2", // Max fee in USDC // CCTP parameters destinationDomain: 19, // HyperEVM domain cctpForwarder: "0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2" as Address, // HyperEVM testnet // HyperCore recipient forwardRecipient: process.env.FORWARD_RECIPIENT as Address, destinationDex: 0, // 0 = perps, 4294967295 = spot // EIP-3009 validity window (seconds) validAfter: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago validBefore: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now }; // -------- Generate Hook Data -------- function encodeForwardHookData( hyperCoreMintRecipient?: `0x${string}`, hyperCoreDestinationDex: number = 0, ): `0x${string}` { if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) { throw new Error("Address must start with 0x"); } const magic = "cctp-forward"; const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0"); const version = "00000000"; if (!hyperCoreMintRecipient) { const dataLength = "00000000"; return `0x${magicHex}${version}${dataLength}`; } const dataLength = "00000018"; const address = hyperCoreMintRecipient.slice(2).toLowerCase(); const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0"); return `0x${magicHex}${version}${dataLength}${address}${dex}`; } // -------- Generate Random Nonce -------- function generateNonce(): Hex { const randomBytes = crypto.getRandomValues(new Uint8Array(32)); return `0x${Array.from(randomBytes) .map((b) => b.toString(16).padStart(2, "0")) .join("")}`; } // -------- Main Function -------- async function main() { // Validate private key and recipient if (!config.privateKey || config.privateKey === "0x") { throw new Error("Set PRIVATE_KEY"); } if (!config.forwardRecipient) { throw new Error("Set FORWARD_RECIPIENT"); } // Setup account and clients const account = privateKeyToAccount(config.privateKey); const publicClient = createPublicClient({ chain: arbitrumSepolia, transport: http(), }); const walletClient = createWalletClient({ chain: arbitrumSepolia, transport: http(), account, }); const amount = parseUnits(config.amount, 6); const maxFee = parseUnits(config.maxFee, 6); console.log("User:", account.address); console.log("Extension:", config.cctpExtension); console.log("USDC:", config.usdcToken); console.log("Total (USDC):", config.amount); console.log( "Dest Domain:", config.destinationDomain, "\nMint Recipient:", config.cctpForwarder, ); console.log("Max Fee (USDC):", config.maxFee, "\nMin Finality:", 1000); // Check USDC balance const balance = await publicClient.readContract({ address: config.usdcToken, abi: [ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], }, ], functionName: "balanceOf", args: [account.address], }); if (balance < amount) { throw new Error( `Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`, ); } // Generate hook data const hookData = encodeForwardHookData( config.forwardRecipient, config.destinationDex, ); console.log( "Forwarder hook enabled -> Final recipient:", config.forwardRecipient, ); console.log("Hook Data:", hookData); // Convert addresses to bytes32 const mintRecipientBytes32 = `0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex; const destinationCallerBytes32 = `0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex; console.log("Destination Caller (bytes32):", destinationCallerBytes32); // Generate nonce for EIP-3009 const nonce = generateNonce(); // Sign EIP-3009 ReceiveWithAuthorization const signature = await walletClient.signTypedData({ domain: { name: "USD Coin", version: "2", chainId: arbitrumSepolia.id, verifyingContract: config.usdcToken, }, types: { ReceiveWithAuthorization: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, ], }, primaryType: "ReceiveWithAuthorization", message: { from: account.address, to: config.cctpExtension, value: amount, validAfter: BigInt(config.validAfter), validBefore: BigInt(config.validBefore), nonce, }, }); // Parse signature into v, r, s const r = signature.slice(0, 66) as Hex; const s = `0x${signature.slice(66, 130)}` as Hex; const v = parseInt(signature.slice(130, 132), 16); // Estimate gas const gasEstimate = await publicClient.estimateContractGas({ address: config.cctpExtension, abi: CCTP_EXTENSION_ABI, functionName: "batchDepositForBurnWithAuth", args: [ { amount, authValidAfter: BigInt(config.validAfter), authValidBefore: BigInt(config.validBefore), authNonce: nonce, v, r, s, }, { amount, destinationDomain: config.destinationDomain, mintRecipient: mintRecipientBytes32, destinationCaller: destinationCallerBytes32, maxFee, minFinalityThreshold: 1000, hookData, }, ], account, }); console.log("Estimated gas:", gasEstimate.toString()); // Execute batchDepositForBurnWithAuth const hash = await walletClient.writeContract({ address: config.cctpExtension, abi: CCTP_EXTENSION_ABI, functionName: "batchDepositForBurnWithAuth", args: [ { amount, authValidAfter: BigInt(config.validAfter), authValidBefore: BigInt(config.validBefore), authNonce: nonce, v, r, s, }, { amount, destinationDomain: config.destinationDomain, mintRecipient: mintRecipientBytes32, destinationCaller: destinationCallerBytes32, maxFee, minFinalityThreshold: 1000, hookData, }, ], gas: (gasEstimate * 120n) / 100n, // +20% }); console.log("Tx hash:", hash); // Wait for transaction receipt const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED"); console.log( "Block:", receipt.blockNumber, "\nGas Used:", receipt.gasUsed.toString(), ); } // Run main().catch((error) => { console.error("Error:", error.message); process.exit(1); }); ``` Run the script: ```bash theme={null} npx tsx --env-file=.env script.ts ``` # Transfer USDC from Ethereum to HyperCore Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-ethereum-to-hypercore This guide shows the steps to transfer USDC from Ethereum to HyperCore using the `TokenMessengerV2` contract with hook data to call the `CctpForwarder` contract on HyperEVM. This CCTP flow follows the same pattern as USDC transfers from Ethereum to any other domain, except for the inclusion of hook data to call the `CctpForwarder` contract on HyperEVM. While this guide uses Ethereum as an example, the same steps apply to any EVM chain that supports CCTP via `TokenMessengerV2`. Adjust the source domain ID and contract addresses for your chain. This guide does not provide full example code, you can find an example of transfers from Ethereum in the [CCTP quickstart](cctp/quickstarts/transfer-usdc-ethereum-to-arc). Fast Transfers from Ethereum to HyperEVM incur a protocol fee and a dynamic forwarding fee for the HyperEVM chain relay transaction. Fast Transfer is the default for transfers from Ethereum to HyperCore. ## Steps Use the following steps to transfer USDC from Ethereum to HyperCore. ### Step 1. Get CCTP fees from the API Query the CCTP API for the fees for transferring USDC from Ethereum to HyperCore. This value is passed to the `maxFee` parameter in the `depositForBurnWithHook` transaction. The following is an example request to the CCTP using source domain 0 (Ethereum) and destination domain 19 (HyperEVM): ```shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/0/19?forward=true&hyperCoreDeposit=true' \ --header 'Content-Type: application/json' ``` **Response:** ```json theme={null} [ { "finalityThreshold": 1000, // fast transfer "minimumFee": 1, // in basis points "forwardFee": { "low": 211203, "med": 216109, // 0.216109 USDC "high": 221014 } }, { "finalityThreshold": 2000, // standard transfer "minimumFee": 0, "forwardFee": { "low": 211203, "med": 216109, // 0.216109 USDC "high": 221014 } } ] ``` ### Step 2. Calculate the USDC amounts minus fees There is a protocol fee to deposit USDC from Ethereum to HyperEVM and a dynamic forwarding fee for the HyperEVM chain relay transaction. The CCTP fast transfer fee is 1 basis point (0.01%) of the transfer amount. The forwarding fee is 0.20 USDC (`0_200_000` subunits) plus a dynamic destination chain gas fee. For a 10 USDC transfer from Ethereum to HyperCore, the protocol fee is 0.001 USDC (10 USDC × 0.0001) and an example forwarding fee is 0.216109 USDC, for a total fee of 0.217109 USDC. Because the protocol fee scales with the transfer amount and the forwarding fee is dynamic, you must recalculate `maxFee` for each transfer. For a programmatic approach, see [`calculateMaxFee`](/cctp/concepts/fees#maximum-fee-parameter) on the fees page. ### Step 3. Approve the USDC transfer To allow the `TokenMessengerV2` contract to transfer the USDC on your behalf, you need to approve the transfer. This is done by calling the `approve` function on the USDC contract. You can see an example of this contract call in the [Ethereum CCTP V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/evm.ts#L63). ### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract Create a `depositForBurnWithHook` transaction for the `TokenMessengerV2` contract with the following parameters: * `amount`: The amount of USDC to transfer * `destinationDomain`: 19 (HyperEVM) * `mintRecipient`: The address of the `CctpForwarder` contract on HyperEVM * `burnToken`: The address of the USDC contract on the source chain * `destinationCaller`: The address of the `CctpForwarder` contract on HyperEVM * `maxFee`: The protocol fee + forwarding fee calculated in Step 2 * `minFinalityThreshold`: `1000` (Fast Transfer) * `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM when you transfer USDC to HyperCore. * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. The `hookData` is the data to execute the forwarder to HyperCore. The following is an example of the hook data: ```ts TypeScript theme={null} /** * Generate CCTP forwarder hook data for HyperCore * * Hook Data Format: * Field Bytes Type Index * magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding * version 4 uint32 24 * dataLength 4 uint32 28 * hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore * hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot) */ function encodeForwardHookData( hyperCoreMintRecipient?: `0x${string}`, hyperCoreDestinationDex: number = 0, ): `0x${string}` { // Validate hex prefix if recipient provided if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) { throw new Error("Address must start with 0x"); } // Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros const magic = "cctp-forward"; const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0"); // Version: uint32 = 0 (4 bytes, big-endian) const version = "00000000"; if (!hyperCoreMintRecipient) { // No recipient: dataLength = 0, return header only (32 bytes) const dataLength = "00000000"; return `0x${magicHex}${version}${dataLength}`; } // With recipient: dataLength = 24 (20 bytes address + 4 bytes dex) const dataLength = "00000018"; // 24 in hex // Address: 20 bytes (remove 0x prefix) const address = hyperCoreMintRecipient.slice(2).toLowerCase(); // Destination DEX: uint32 big-endian // 0 = perps, 4294967295 (0xFFFFFFFF) = spot const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0"); return `0x${magicHex}${version}${dataLength}${address}${dex}`; } ``` You can see an example of this contract call in the [Ethereum V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/evm.ts#L105) Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and automatically forwarded to your address on HyperCore. By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps balance on HyperCore. To deposit to the spot balance, set `hyperCoreDestinationDex` to `4294967295` (uint32 max value). # Transfer USDC from HyperEVM to HyperCore Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-hyperevm-to-hypercore This guide shows how to transfer USDC from HyperEVM to HyperCore using the `CoreDepositWallet` contract. **Tip:** The `CoreDepositWallet` contract provides `deposit`, `depositFor`, and `depositWithAuth` methods. This guide uses `deposit`. All methods accept a `destinationDex` parameter (`0` for perps, `4294967295` for spot). See the [CoreDepositWallet contract interface](/cctp/references/coredepositwallet-contract-interface) for detailed information. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM testnet wallet with the private key available * Funded your wallet with HyperEVM testnet USDC from the [Circle Faucet](https://faucet.circle.com) * Created a new Node project and installed dependencies: ```bash theme={null} npm install viem npm install -D tsx typescript @types/node ``` * Created a `.env` file with required environment variable: ```text theme={null} PRIVATE_KEY=0x... ``` ## Steps Use the following steps to transfer USDC from HyperEVM to HyperCore. ### Step 1. Approve the `CoreDepositWallet` to spend USDC Approve the `CoreDepositWallet` contract to transfer USDC on your behalf: ```ts TypeScript theme={null} const hash = await walletClient.writeContract({ address: USDC_ADDRESS, abi: USDC_ABI, functionName: "approve", args: [CORE_DEPOSIT_WALLET, amount], }); await publicClient.waitForTransactionReceipt({ hash }); ``` ### Step 2. Call the `deposit` function Call the `deposit` function with your desired amount and destination: ```ts TypeScript theme={null} const hash = await walletClient.writeContract({ address: CORE_DEPOSIT_WALLET, abi: CORE_DEPOSIT_WALLET_ABI, functionName: "deposit", args: [amount, destinationDex], // 0 = perps, 4294967295 = spot }); const receipt = await publicClient.waitForTransactionReceipt({ hash }); ``` The `deposit` function transfers USDC from your account to the `CoreDepositWallet` and credits your HyperCore balance. ## Full example code The following is a complete example of how to transfer USDC from HyperEVM to HyperCore. ```ts script.ts expandable theme={null} /** * Script: Call CoreDepositWallet.deposit on HyperEVM * - Approves USDC spending * - Calls deposit(amount, destinationDex) */ import { createWalletClient, createPublicClient, http, parseUnits, formatUnits, type Address, type Hex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { hyperliquidEvmTestnet } from "viem/chains"; // -------- Configuration -------- const config = { privateKey: (process.env.PRIVATE_KEY || "0x") as Hex, // Contract addresses (HyperEVM Testnet) coreDepositWallet: "0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206" as Address, usdcToken: "0x2B3370eE501B4a559b57D449569354196457D8Ab" as Address, // Transfer parameters amount: "2", // USDC amount to deposit // HyperCore destination (0 = perps, 4294967295 = spot) destinationDex: 0, }; // -------- Main Function -------- async function main() { // Validate private key if (!config.privateKey || config.privateKey === "0x") { throw new Error("Set PRIVATE_KEY"); } // Setup account and clients const account = privateKeyToAccount(config.privateKey); const publicClient = createPublicClient({ chain: hyperliquidEvmTestnet, transport: http(), }); const walletClient = createWalletClient({ chain: hyperliquidEvmTestnet, transport: http(), account, }); const amount = parseUnits(config.amount, 6); console.log("User:", account.address); console.log("CoreDepositWallet:", config.coreDepositWallet); console.log("USDC:", config.usdcToken); console.log("Amount (USDC):", config.amount); console.log( "Destination DEX:", config.destinationDex === 0 ? "perps" : "spot", ); // Check USDC balance const balance = await publicClient.readContract({ address: config.usdcToken, abi: [ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], }, ], functionName: "balanceOf", args: [account.address], }); if (balance < amount) { throw new Error( `Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`, ); } // Check current allowance const currentAllowance = await publicClient.readContract({ address: config.usdcToken, abi: [ { name: "allowance", type: "function", stateMutability: "view", inputs: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, ], outputs: [{ name: "", type: "uint256" }], }, ], functionName: "allowance", args: [account.address, config.coreDepositWallet], }); // Step 1: Approve if needed if (currentAllowance < amount) { console.log("\nApproving USDC spending..."); const hash = await walletClient.writeContract({ address: config.usdcToken, abi: [ { name: "approve", type: "function", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [config.coreDepositWallet, amount], }); console.log("Approve tx hash:", hash); await publicClient.waitForTransactionReceipt({ hash }); console.log("Approval confirmed"); } else { console.log("\nSufficient allowance already exists"); } // Step 2: Deposit console.log("\nDepositing USDC to HyperCore..."); const hash = await walletClient.writeContract({ address: config.coreDepositWallet, abi: [ { name: "deposit", type: "function", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDex", type: "uint32" }, ], outputs: [], }, ], functionName: "deposit", args: [amount, config.destinationDex], }); console.log("Deposit tx hash:", hash); // Wait for transaction receipt const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED"); console.log( "Block:", receipt.blockNumber, "\nGas Used:", receipt.gasUsed.toString(), ); } // Run main().catch((error) => { console.error("Error:", error.message); process.exit(1); }); ``` Run the script: ```bash theme={null} npx tsx --env-file=.env script.ts ``` # Transfer USDC from Solana to HyperCore Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-solana-to-hypercore This guide shows how to transfer USDC from Solana to HyperCore using the `TokenMessengerV2` contract. Solana's CCTP implementation does not have the `depositForBurnWithAuth`, and there is no `CctpExtension` contract for Solana. As such, transfers from Solana to HyperCore follow the standard CCTP flow, with the addition of hook data to call the `CctpForwarder` contract on HyperEVM. This guide does not provide full example code for the transfer to HyperCore from Solana. Fast Transfers from Solana to HyperEVM incur a protocol fee and a dynamic forwarding fee for the HyperEVM chain relay transaction. Fast Transfer is the default for transfers from Solana to HyperCore. ## Steps Use the following steps to transfer USDC from Solana to HyperCore. ### Step 1. Get CCTP fees from the API Query the CCTP API for the fees for transferring USDC from Solana to HyperCore. This value is passed to the `maxFee` parameter in the `depositForBurnWithHook` transaction. The following is an example request to the CCTP using source domain 5 (Solana) and destination domain 19 (HyperEVM): ```shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/19?forward=true&hyperCoreDeposit=true' \ --header 'Content-Type: application/json' ``` **Response:** ```json theme={null} [ { "finalityThreshold": 1000, // fast transfer "minimumFee": 1, // in basis points "forwardFee": { "low": 211203, "med": 216109, // 0.216109 USDC "high": 221014 } }, { "finalityThreshold": 2000, // standard transfer "minimumFee": 0, "forwardFee": { "low": 211203, "med": 216109, // 0.216109 USDC "high": 221014 } } ] ``` ### Step 2. Calculate the USDC amounts minus fees There is a protocol fee to deposit USDC from Solana to HyperEVM and a dynamic forwarding fee for the HyperEVM chain relay transaction. The CCTP fast transfer fee is 1 basis point (0.01%) of the transfer amount. The forwarding fee is 0.20 USDC (`0_200_000` subunits) plus a dynamic destination chain gas fee. For a 10 USDC transfer from Solana to HyperCore, the protocol fee is 0.001 USDC (10 USDC × 0.0001) and an example forwarding fee is 0.216109 USDC, for a total fee of 0.217109 USDC. Because the protocol fee scales with the transfer amount and the forwarding fee is dynamic, you must recalculate `maxFee` for each transfer. For a programmatic approach, see [`calculateMaxFee`](/cctp/concepts/fees#maximum-fee-parameter) on the fees page. ### Step 3. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract Create a `depositForBurnWithHook` transaction for the `TokenMessengerV2` contract with the following parameters: * `amount`: The amount of USDC to transfer * `destinationDomain`: 19 (HyperEVM) * `mintRecipient`: The address of the `CctpForwarder` contract on HyperEVM * `destinationCaller`: The address of the `CctpForwarder` contract on HyperEVM * `maxFee`: The protocol fee + forwarding fee calculated in Step 2 * `minFinalityThreshold`: `1000` (Fast Transfer) * `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM when you transfer USDC to HyperCore. * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. The `hookData` is the data to execute the forwarder to HyperCore. The following is an example of the hook data: ```ts TypeScript theme={null} /** * Generate CCTP forwarder hook data for HyperCore * * Hook Data Format: * Field Bytes Type Index * magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding * version 4 uint32 24 * dataLength 4 uint32 28 * hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore * hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot) */ function encodeForwardHookData( hyperCoreMintRecipient?: `0x${string}`, hyperCoreDestinationDex: number = 0, ): `0x${string}` { // Validate hex prefix if recipient provided if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) { throw new Error("Address must start with 0x"); } // Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros const magic = "cctp-forward"; const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0"); // Version: uint32 = 0 (4 bytes, big-endian) const version = "00000000"; if (!hyperCoreMintRecipient) { // No recipient: dataLength = 0, return header only (32 bytes) const dataLength = "00000000"; return `0x${magicHex}${version}${dataLength}`; } // With recipient: dataLength = 24 (20 bytes address + 4 bytes dex) const dataLength = "00000018"; // 24 in hex // Address: 20 bytes (remove 0x prefix) const address = hyperCoreMintRecipient.slice(2).toLowerCase(); // Destination DEX: uint32 big-endian // 0 = perps, 4294967295 (0xFFFFFFFF) = spot const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0"); return `0x${magicHex}${version}${dataLength}${address}${dex}`; } ``` For full example code calling the `depositForBurnWithHook` function, see the [Solana CCTP V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/solana.ts#L94). Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and automatically forwarded to your address on HyperCore. By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps balance on HyperCore. To deposit to the spot balance, set `hyperCoreDestinationDex` to `4294967295` (uint32 max value). # How-to: Transfer USDC with the Forwarding Service Source: https://developers.circle.com/cctp/howtos/transfer-usdc-with-forwarding-service Transfer USDC Crosschain with the Circle Forwarding Service This guide shows how to transfer USDC crosschain using the [Circle Forwarding Service](/cctp/concepts/forwarding-service). This example shows a transfer from Base Sepolia to Avalanche Fuji, but you can use the same steps to transfer to any supported [destination blockchain](/cctp/concepts/supported-chains-and-domains), including Solana. When you use the Forwarding Service, Circle handles the mint transaction on the destination blockchain, eliminating the need for you to hold native tokens for gas on the destination blockchain or run multichain infrastructure. ## Prerequisites Before you start, ensure you have: * Installed [Node.js v22+](https://nodejs.org/) * Created a TypeScript project and installed the `viem` package. * Created a wallet with the private key available on the source chain. * Funded the wallet with testnet USDC and native tokens for gas fees on the source chain. * Created a `.env` file with your private key and recipient address. ## Steps Use the following steps to transfer USDC with the Forwarding Service. ### Step 1. Get CCTP fees from the API Query the CCTP API for the fees for transferring USDC from Base Sepolia to Avalanche Fuji. This value is passed to the `maxFee` parameter in the `depositForBurnWithHook` transaction. The following is an example request using source domain 6 (Base Sepolia) and destination domain 1 (Avalanche Fuji): ```typescript theme={null} const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/1?forward=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); const fees = await response.json(); console.log(fees); ``` **Example response:** ```json theme={null} [ { "finalityThreshold": 1000, // Fast transfer "minimumFee": 1.3, // Basis points (0.013% fee rate) "forwardFee": { // Gas-based, fluctuates based on destination chain gas prices "low": 206035, // 0.206035 USDC "med": 207543, // 0.207543 USDC "high": 209052 // 0.209052 USDC } }, { "finalityThreshold": 2000, // Standard transfer "minimumFee": 0, // No fee "forwardFee": { "low": 206035, // 0.206035 USDC "med": 207543, // 0.207543 USDC "high": 209052 // 0.209052 USDC } } ] ``` The `forwardFee` is the fee charged by the Forwarding Service. The `minimumFee` is the CCTP protocol fee rate in basis points, applied as a percentage of the transfer amount. Circle recommends selecting the `med` fee level or higher from the `forwardFee` object in the API response. Note that `forwardFee` values fluctuate based on destination chain gas prices. Make the query immediately before initiating your transfer. When the destination blockchain is Solana, the `forwardFee` values include both gas and rent costs. If the recipient does not have an existing USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account), add `includeRecipientSetup=true` to the fee query so the returned fee covers ATA creation: ```typescript theme={null} // Source domain 6 (Base Sepolia), destination domain 5 (Solana) const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/5?forward=true&includeRecipientSetup=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); ``` Unlike EVM destinations, the `mintRecipient` for Solana must be the recipient's **USDC token account address** (ATA), not the wallet address. Derive the ATA from the wallet address and the USDC mint: ```typescript theme={null} import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; const recipientWallet = new PublicKey("RecipientSolanaWalletAddress"); const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet); const mintRecipientBytes32 = `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}` as `0x${string}`; ``` If the recipient does not have an existing ATA and you passed `includeRecipientSetup=true` in the fee query, you must also encode ATA creation fields in the hook data. See [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation) for the extended hook data format. ### Step 2. Calculate the USDC amounts and fees Calculate the total fee by combining the protocol fee and the Forwarding Service fee. The `maxFee` parameter must cover both fees for the transfer to succeed. ```typescript theme={null} // Amount to transfer (10 USDC in subunits) const transferAmount = 10_000_000n; // Parse fees from API response const feeData = fees[0]; // Use fast transfer fees (finalityThreshold: 1000) const forwardFee = BigInt(feeData.forwardFee.med); // Calculate protocol fee (minimumFee is in basis points) const minimumFeeBps = feeData.minimumFee; const protocolFee = (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n; // Total max fee should cover both fees const maxFee = forwardFee + protocolFee; const totalAmount = transferAmount + maxFee; // Total to burn console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC"); console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); ``` In this example, for a 10 USDC transfer with forwarding, the total fee is 0.208843 USDC (0.207543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol fee). For the recipient to receive 10 USDC, you must burn 10.208843 USDC in total. If the `maxFee` parameter is insufficient to cover the both Fast Transfer protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding execution over Fast Transfer. This means that the transfer will execute as a Standard Transfer with the Forwarding Service. ### Step 3. Approve the USDC transfer Grant approval for the [`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on Base to transfer USDC from your wallet. Approve at least `totalAmount` (including fees) calculated in Step 2. ```typescript theme={null} import { createWalletClient, http, encodeFunctionData } from "viem"; import { baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Configuration const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; const BASE_SEPOLIA_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; // Set up wallet client const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const client = createWalletClient({ chain: baseSepolia, transport: http(), account, }); async function approveUSDC(amount: bigint) { console.log("Approving USDC transfer..."); const approveTx = await client.sendTransaction({ to: BASE_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [BASE_SEPOLIA_TOKEN_MESSENGER, amount], }), }); console.log("USDC Approval Tx:", approveTx); return approveTx; } // Approve the total amount (from Step 2) await approveUSDC(totalAmount); ``` ### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract Create and send a `depositForBurnWithHook` transaction with the Forwarding Service hook data. The hook data tells the CCTP Forwarding Service to automatically forward the mint transaction on the destination chain. The Forwarding Service hook data is a static 32-byte value containing the magic bytes `cctp-forward`, version `0`, and length `0`. For details on the hook format, see [Forwarding Service hook format](/cctp/concepts/forwarding-service#hook-format). ```typescript theme={null} // Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0) const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000"; ``` When forwarding to Solana, use the same static hook data if the recipient already has a USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account). If the recipient does not have an ATA and you included `includeRecipientSetup=true` in the fee query (see Step 1), construct extended hook data that requests ATA creation: ```typescript theme={null} import { PublicKey } from "@solana/web3.js"; // Magic bytes "cctp-forward" padded to 24 bytes const magicBytes = Buffer.alloc(24); magicBytes.write("cctp-forward", "utf-8"); // Version (uint32, big-endian) = 0 const version = Buffer.alloc(4); // Length of additional Circle hook data (uint32, big-endian) = 33 const length = Buffer.alloc(4); length.writeUInt32BE(33); // ATA creation flag = 1 const ataFlag = Buffer.from([1]); // Recipient wallet address (32 bytes) const recipientWallet = new PublicKey("RecipientSolanaWalletAddress"); const walletBytes = Buffer.from(recipientWallet.toBytes()); const FORWARDING_SERVICE_HOOK_DATA = ("0x" + Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString( "hex", )) as `0x${string}`; ``` For the full hook data format, see [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation). Then, send the `depositForBurnWithHook` transaction: Use `totalAmount` (transfer amount + fees) for the `amount` parameter. The recipient receives only the transfer amount after fees are deducted. ```typescript theme={null} import { pad, encodeFunctionData } from "viem"; // Configuration const AVALANCHE_FUJI_DOMAIN = 1; const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS" as `0x${string}`; // Convert address to bytes32 format const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 }); async function depositForBurnWithHook() { console.log("Burning USDC on Base with Forwarding Service hook..."); const burnTx = await client.sendTransaction({ to: BASE_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ totalAmount, // Total to burn (recipient receives transferAmount after fees) AVALANCHE_FUJI_DOMAIN, mintRecipientBytes32, BASE_SEPOLIA_USDC, pad("0x", { size: 32 }), // destinationCaller (empty = any caller) maxFee, 1000, FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log("Burn Tx:", burnTx); return burnTx; } ``` Once the burn transaction is confirmed on Base, the Circle Forwarding Service automatically handles the attestation and mint transaction on Avalanche. The USDC is minted directly to the `mintRecipient` address on the destination chain. The recipient receives `transferAmount` USDC (fees are automatically deducted from the `totalAmount` on the destination chain). ### Step 5. Verify the mint transaction After the burn transaction 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 chain. The attestation may take time to become available, depending on the destination chain. Poll the API until the message is ready: ```typescript theme={null} // Configuration const BASE_SEPOLIA_DOMAIN = 6; process.stdout.write("Waiting for attestation..."); let mintTx; while (!mintTx) { const messageResponse = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`, ); const data = await messageResponse.json(); if (data.messages?.[0]?.forwardTxHash) { mintTx = data.messages[0].forwardTxHash; console.log(); // New line after dots } else { process.stdout.write("."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } console.log("Mint Tx:", mintTx); ``` ## Full example code The following is a complete example of how to transfer USDC from Base Sepolia to Avalanche Fuji using the Forwarding Service. Remember to set the `PRIVATE_KEY` and `DESTINATION_ADDRESS` environment variables. ```typescript script.ts expandable theme={null} import { createWalletClient, http, encodeFunctionData, pad } from "viem"; import { baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Validate environment variables if (!process.env.PRIVATE_KEY || !process.env.DESTINATION_ADDRESS) { throw new Error( "PRIVATE_KEY and DESTINATION_ADDRESS environment variables are required", ); } // Configuration const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; const BASE_SEPOLIA_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const BASE_SEPOLIA_DOMAIN = 6; const AVALANCHE_FUJI_DOMAIN = 1; const DESTINATION_ADDRESS = process.env.DESTINATION_ADDRESS as `0x${string}`; // Forwarding Service hook data const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; // Set up wallet client const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const client = createWalletClient({ chain: baseSepolia, transport: http(), account, }); async function main() { console.log("Wallet address:", account.address); console.log("Destination address:", DESTINATION_ADDRESS); // Step 1: Get fees from API console.log("\nStep 1: Getting CCTP fees..."); const feeResponse = await fetch( `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${BASE_SEPOLIA_DOMAIN}/${AVALANCHE_FUJI_DOMAIN}?forward=true`, { method: "GET", headers: { "Content-Type": "application/json" }, }, ); const fees = await feeResponse.json(); console.log("Fees:", JSON.stringify(fees, null, 2)); // Step 2: Calculate amounts console.log("\nStep 2: Calculating amounts..."); const transferAmount = 10_000_000n; // 10 USDC const feeData = fees[0]; // Fast transfer const forwardFee = BigInt(feeData.forwardFee.med); const minimumFeeBps = feeData.minimumFee; const protocolFee = (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = transferAmount + maxFee; // Total to burn console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC"); console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); // Step 3: Approve USDC console.log("\nStep 3: Approving USDC transfer..."); const approveTx = await client.sendTransaction({ to: BASE_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [BASE_SEPOLIA_TOKEN_MESSENGER, totalAmount], }), }); console.log("Approval Tx:", approveTx); // Step 4: Burn USDC with Forwarding Service hook console.log("\nStep 4: Burning USDC with Forwarding Service hook..."); const burnTx = await client.sendTransaction({ to: BASE_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ totalAmount, AVALANCHE_FUJI_DOMAIN, pad(DESTINATION_ADDRESS as `0x${string}`, { size: 32 }), BASE_SEPOLIA_USDC, pad("0x", { size: 32 }), maxFee, 1000, // Fast Transfer FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log("Burn Tx:", burnTx); console.log( "\nTransfer initiated. The Forwarding Service will automatically mint USDC on Avalanche.", ); // Step 5: Verify the mint transaction console.log("\nStep 5: Verifying mint transaction..."); process.stdout.write("Waiting for attestation..."); let mintTx; while (!mintTx) { const messageResponse = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`, ); const data = await messageResponse.json(); if (data.messages?.[0]?.forwardTxHash) { mintTx = data.messages[0].forwardTxHash; console.log("\n"); // New line after dots } else { process.stdout.write("."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } console.log("Mint Tx:", mintTx); } main().catch(console.error); ``` Run the script: ```bash theme={null} npx tsx script.ts ``` # Troubleshoot CCTP Transfers Source: https://developers.circle.com/cctp/howtos/troubleshoot-transfers Diagnose and resolve stuck or failed crosschain USDC transfers This guide helps you diagnose and resolve issues with CCTP transfers that appear stuck or fail to complete. A CCTP transfer involves three stages, and problems can occur at any point in the process. ## Transfer stages A CCTP transfer consists of three stages: 1. **Burn**: USDC is burned on the source blockchain 2. **Attestation**: Circle's Attestation Service observes the burn and signs a message 3. **Mint**: The signed attestation is submitted to mint USDC on the destination blockchain If your transfer appears stuck, first identify which stage has the issue. ## Identify where your transfer is stuck Use the following steps to determine the current state of your transfer: Verify the burn transaction succeeded on the source blockchain using a block explorer. If the transaction failed or is pending, the issue is at the burn stage. Call the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint with your transaction hash. Interpret the response: * **404 response**: The attestation service hasn't observed the burn yet. This is normal and expected. See [Why 404 responses are expected](/cctp/howtos/resolve-stuck-attestation#why-404-responses-are-expected). * **Empty `messages` array**: The burn exists but hasn't been processed yet. * **Status `pending`**: The burn is awaiting block confirmations. * **Status `complete` with attestation**: The attestation is ready. If your transfer is stuck, the issue is at the mint stage. If you have an attestation but your destination wallet doesn't have the USDC, either: * The mint transaction was never submitted * The mint transaction failed Check your destination blockchain for any failed `receiveMessage` transactions. ## Common issues and solutions | Issue | Cause | Solution | | ------------------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------- | | 404 persists for extended time | Burn transaction may have failed or is still confirming | Verify burn succeeded on block explorer, if it failed, retry the burn | | Attestation status remains `pending` | Waiting for block confirmations | Wait for sufficient confirmations based on finality threshold | | Have attestation but mint fails | Gas issues, incorrect parameters, or nonce already used | See [Retry a Failed Mint](/cctp/howtos/retry-failed-mint) | # Withdraw USDC from HyperCore to EVM chains Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-evm This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance to an external EVM blockchain (such as Arbitrum, Ethereum, or Base) using the HyperCore API. Withdrawals from HyperCore to EVM chains default to the Fast Transfer method, due to the fast finality of HyperEVM. The withdrawal process: 1. Debits your HyperCore balance (`spot` or `perp`) 2. Routes through HyperEVM where USDC is burned via CCTP 3. CCTP attests to the burn and mints on the destination chain 4. If automatic forwarding is enabled, the recipient receives funds directly Withdrawals include a HyperCore fee and (if using the Forwarding Service) a CCTP forwarding fee. Ensure your withdrawal amount exceeds combined fees depending on your transfer. ## Important considerations Keep these things in mind when withdrawing USDC from HyperCore to EVM chains: * **Data field:** If the data field is empty, the `CoreDepositWallet` automatically sets a default hook that enables automatic message forwarding on the destination blockchain, provided that the blockchain supports CCTP forwarding. If the data field is not empty, its contents are passed to the CCTP protocol as the value of the `hookData` field. * **Destination caller:** The CCTP `destinationCaller` is always set to the zero address. Passing your own hook data means that anyone can receive the message on the destination blockchain. * **Withdrawal fees:** In addition to the `maxFee` charged by the HyperCore blockchain, an additional fixed forwarding fee may be charged by CCTP if automatic forwarding is enabled. The forwarding fee amount depends on the destination blockchain and can be viewed by querying the `CoreDepositWallet` smart contract. Initially, the fee for forwarding to Arbitrum is 0.2 USDC. If the withdrawal includes custom hook data, the forwarding fee is not set and users have to receive the message on the destination blockchain themselves. * **Minimum withdrawal amount:** If the withdrawal amount is less than the required forwarding fee, the transaction on HyperEVM reverts. Make sure the withdrawal amount is larger than the fees. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM wallet with the private key available * Funded your HyperCore account with USDC in either `spot` or `perp` balance * Created a new Node project and installed dependencies: ```bash theme={null} npm install ethers npm install -D tsx typescript @types/node ``` * Created a `.env` file with required environment variables: ```text theme={null} PRIVATE_KEY=0x... DESTINATION_RECIPIENT=0x... # Recipient address on destination chain ``` ## Steps Use the following steps to withdraw USDC from HyperCore to an EVM blockchain. ### Step 1. Construct the `sendToEvmWithData` action Create a `sendToEvmWithData` action object with the following parameters: * `type`: `sendToEvmWithData` * `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet) * `signatureChainId`: The destination chain's EVM chain ID in hexadecimal format (e.g., `"0xa4b1"` for Arbitrum, `"0x1"` for Ethereum). Must match the destination chain. * `token`: `USDC` * `amount`: The amount of USDC as a string (e.g., `"10"` for 10 USDC, `"1.5"` for 1.5 USDC) * `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for `perp` balance * `destinationRecipient`: The recipient address on the destination blockchain * `addressEncoding`: `hex` for EVM chains or `base58` for Solana * `destinationChainId`: The CCTP destination domain ID (for example, `3` for Arbitrum, `0` for Ethereum, `6` for Base) * `gasLimit`: Gas limit for the transaction on the destination chain * `data`: CCTP hook data (use `"0x"` for automatic forwarding) * `nonce`: Current timestamp in milliseconds ```ts TypeScript theme={null} // Example action payload for sendToEvmWithData const action = { type: "sendToEvmWithData", hyperliquidChain: "Mainnet", signatureChainId: "0xa4b1", // Arbitrum chain ID used for signing token: "USDC", amount: "10", // 10 USDC sourceDex: "spot", // or "" for perp destinationRecipient: "0x1234567890123456789012345678901234567890", addressEncoding: "hex", destinationChainId: 3, // Arbitrum CCTP domain gasLimit: 200000, data: "0x", // "0x" enables automatic forwarding on the destination nonce: Date.now(), }; ``` ### Step 2. Sign the action using EIP-712 Sign the action using the EIP-712 typed data signing standard. The signature proves that you authorize this withdrawal. The signing domain should include: * `name`: `"HyperliquidSignTransaction"` * `version`: `"1"` * `chainId`: The chain ID from `signatureChainId` (as a number) * `verifyingContract`: `"0x0000000000000000000000000000000000000000"` ```ts TypeScript theme={null} import { Wallet, Signature } from "ethers"; // Sign the action using EIP-712 const wallet = new Wallet(privateKey); const chainId = parseInt(signatureChainId, 16); const domain = { name: "HyperliquidSignTransaction", version: "1", chainId, verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { "HyperliquidTransaction:SendToEvmWithData": [ { name: "hyperliquidChain", type: "string" }, { name: "token", type: "string" }, { name: "amount", type: "string" }, { name: "sourceDex", type: "string" }, { name: "destinationRecipient", type: "string" }, { name: "addressEncoding", type: "string" }, { name: "destinationChainId", type: "uint32" }, { name: "gasLimit", type: "uint64" }, { name: "data", type: "bytes" }, { name: "nonce", type: "uint64" }, ], }; const message = { hyperliquidChain: "Mainnet", token: "USDC", amount: "10", sourceDex: "spot", destinationRecipient: "0x...", addressEncoding: "hex", destinationChainId: 3, gasLimit: BigInt(200000), data: "0x", nonce: BigInt(Date.now()), }; const sigHex = await wallet.signTypedData(domain, types, message); const sig = Signature.from(sigHex); const signature = { r: sig.r, s: sig.s, v: sig.v }; ``` ### Step 3. Submit the signed action to the exchange API Call the [exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint) endpoint with the action, nonce, and signature. ```ts TypeScript theme={null} const response = await fetch("https://api.hyperliquid.xyz/exchange", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, nonce: timestamp, signature, }), }); const result = await response.json(); if (response.status === 200 && result.status === "ok") { console.log("Withdrawal initiated successfully:", result); } else { throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`); } ``` ## Full example code The following is a complete example of how to withdraw USDC from HyperCore to an external EVM blockchain. By default, it withdraws 10 USDC from your perp balance to Arbitrum testnet with automatic forwarding enabled. For other destination chains, update `destinationChainId` (CCTP domain ID) and `signatureChainId` (destination chain's EVM chain ID in hex) accordingly. ```ts TypeScript expandable theme={null} /** * Script: Withdraw USDC from HyperCore to EVM chain * - Signs EIP-712 sendToEvmWithData action * - Submits to Hyperliquid /exchange API */ import { Wallet, Signature } from "ethers"; // -------- Configuration -------- const config = { privateKey: process.env.PRIVATE_KEY as string, // Transfer parameters amount: process.env.AMOUNT || "10", // 10 USDC sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot // Destination parameters destinationRecipient: process.env.DESTINATION_RECIPIENT as string, destinationChainId: Number(process.env.DESTINATION_CHAIN_ID || 3), // 3 = Arbitrum addressEncoding: process.env.ADDRESS_ENCODING || "hex", gasLimit: Number(process.env.GAS_LIMIT || 200000), data: process.env.DATA || "0x", // "0x" enables automatic forwarding // Hyperliquid environment isMainnet: String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true", signatureChainId: "0xa4b1", // Destination chain's EVM chain ID (hex) for EIP-712 signing }; // -------- Main Function -------- async function main() { // Validate required parameters if (!config.privateKey) { throw new Error("Set PRIVATE_KEY"); } if (!config.destinationRecipient) { throw new Error("Set DESTINATION_RECIPIENT"); } const apiUrl = config.isMainnet ? "https://api.hyperliquid.xyz" : "https://api.hyperliquid-testnet.xyz"; const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet"; const chainId = parseInt(config.signatureChainId, 16); const timestamp = Date.now(); console.log("Withdrawing from HyperCore:", hyperliquidChain); console.log("Source balance:", config.sourceDex || "perp"); console.log("Amount (USDC):", config.amount); console.log("Destination recipient:", config.destinationRecipient); console.log("Destination chain ID:", config.destinationChainId); console.log("Gas limit:", config.gasLimit); // EIP-712 Domain const domain = { name: "HyperliquidSignTransaction", version: "1", chainId, verifyingContract: "0x0000000000000000000000000000000000000000", }; // EIP-712 Types const types = { "HyperliquidTransaction:SendToEvmWithData": [ { name: "hyperliquidChain", type: "string" }, { name: "token", type: "string" }, { name: "amount", type: "string" }, { name: "sourceDex", type: "string" }, { name: "destinationRecipient", type: "string" }, { name: "addressEncoding", type: "string" }, { name: "destinationChainId", type: "uint32" }, { name: "gasLimit", type: "uint64" }, { name: "data", type: "bytes" }, { name: "nonce", type: "uint64" }, ], }; // Message to sign const message = { hyperliquidChain, token: "USDC", amount: config.amount, sourceDex: config.sourceDex, destinationRecipient: config.destinationRecipient, addressEncoding: config.addressEncoding, destinationChainId: config.destinationChainId, gasLimit: BigInt(config.gasLimit), data: config.data, nonce: BigInt(timestamp), }; // Sign the message using EIP-712 const wallet = new Wallet(config.privateKey); const sigHex = await wallet.signTypedData(domain, types, message); const sig = Signature.from(sigHex); // Build action payload const action = { type: "sendToEvmWithData", hyperliquidChain, signatureChainId: config.signatureChainId, token: "USDC", amount: config.amount, sourceDex: config.sourceDex, destinationRecipient: config.destinationRecipient, addressEncoding: config.addressEncoding, destinationChainId: config.destinationChainId, gasLimit: config.gasLimit, data: config.data, nonce: timestamp, }; // Submit to Hyperliquid exchange API const response = await fetch(`${apiUrl}/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, nonce: timestamp, signature: { r: sig.r, s: sig.s, v: sig.v }, }), }); const result = await response.json(); console.log("\nStatus:", response.status); console.log("Response:", JSON.stringify(result, null, 2)); if (response.status === 200 && result.status === "ok") { console.log("\nWithdrawal initiated successfully"); } else { throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`); } } // Run main().catch((error) => { console.error("Error:", error.message); process.exit(1); }); ``` Run the script: ```bash theme={null} npx tsx script.ts ``` # Withdraw USDC from HyperCore to HyperEVM Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-hyperevm This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance to HyperEVM using the HyperCore API. You can only withdraw USDC from HyperCore to the same address on HyperEVM. It's not possible to specify a different recipient address. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM wallet with the private key available * Funded your HyperCore account with USDC in either `spot` or `perp` balance * Created a new Node project and installed dependencies: ```bash theme={null} npm install ethers npm install -D tsx typescript @types/node ``` * Created a `.env` file with required environment variables: ```text theme={null} PRIVATE_KEY=0x... ``` ## Steps Use the following steps to withdraw USDC from HyperCore to HyperEVM. ### Step 1. Construct the `sendAsset` action Create a `sendAsset` action object with the following parameters: * `type`: `sendAsset` * `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet) * `signatureChainId`: An EVM chain ID used for EIP-712 replay protection. Must match between signing and the action payload, but can be any valid chain ID (for example, `"0xa4b1"` for Arbitrum) * `destination`: The USDC token system address (`0x2000000000000000000000000000000000000000`) * `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for perp balance * `destinationDex`: `"spot"` * `token`: `USDC` * `amount`: The amount of USDC as a human-readable string (for example, `"10"` for 10 USDC) * `fromSubAccount`: Set to `""` for main account, or the subaccount address * `nonce`: Current timestamp in milliseconds ```ts TypeScript theme={null} const action = { type: "sendAsset", hyperliquidChain: "Testnet", signatureChainId: "0xa4b1", // EVM chain ID for EIP-712 replay protection destination: "0x2000000000000000000000000000000000000000", sourceDex: "", // "" for perp, "spot" for spot destinationDex: "spot", token: "USDC", amount: "10", // 10 USDC (human-readable) fromSubAccount: "", nonce: Date.now(), }; ``` ### Step 2. Sign the action using EIP-712 Sign the action using the EIP-712 typed data signing standard. The signature proves that you authorize this withdrawal. The signing domain should include: * `name`: `"HyperliquidSignTransaction"` * `version`: `"1"` * `chainId`: The chain ID from `signatureChainId` (as a number) * `verifyingContract`: `"0x0000000000000000000000000000000000000000"` ```ts TypeScript theme={null} import { Wallet, Signature } from "ethers"; async function signSendAssetAction( action: any, privateKey: string, ): Promise<{ r: string; s: string; v: number }> { const wallet = new Wallet(privateKey); // Convert chainId from hex to number const chainId = parseInt(action.signatureChainId, 16); // EIP-712 domain const domain = { name: "HyperliquidSignTransaction", version: "1", chainId, verifyingContract: "0x0000000000000000000000000000000000000000", }; // EIP-712 types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES) const types = { "HyperliquidTransaction:SendAsset": [ { name: "hyperliquidChain", type: "string" }, { name: "destination", type: "string" }, { name: "sourceDex", type: "string" }, { name: "destinationDex", type: "string" }, { name: "token", type: "string" }, { name: "amount", type: "string" }, { name: "fromSubAccount", type: "string" }, { name: "nonce", type: "uint64" }, ], }; // Message to sign (only fields defined in EIP-712 types, not signatureChainId) const value = { hyperliquidChain: action.hyperliquidChain, destination: action.destination, sourceDex: action.sourceDex, destinationDex: action.destinationDex, token: action.token, amount: action.amount, fromSubAccount: action.fromSubAccount, nonce: BigInt(action.nonce), }; // Sign the typed data const signature = await wallet.signTypedData(domain, types, value); // Split signature into r, s, v components const sig = Signature.from(signature); return { r: sig.r, s: sig.s, v: sig.v, }; } ``` ### Step 3. Submit the signed action to the exchange API Call the [exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#send-asset) endpoint with the action, nonce, and signature. ```ts TypeScript theme={null} async function submitSendAsset( action: any, signature: { r: string; s: string; v: number }, ) { // Use https://api.hyperliquid.xyz for mainnet const response = await fetch("https://api.hyperliquid-testnet.xyz/exchange", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ action: action, nonce: action.nonce, signature: signature, }), }); const data = await response.json(); if (data.status === "ok") { console.log("Withdrawal successful:", data); return data; } else { throw new Error(`Withdrawal failed: ${JSON.stringify(data)}`); } } ``` ## Full example code The following is a complete example of how to withdraw USDC from HyperCore to HyperEVM. By default, it withdraws 10 USDC from your perp balance to HyperEVM testnet. ```ts TypeScript expandable theme={null} /** * Script: Withdraw USDC from HyperCore to HyperEVM * - Constructs a SendAsset action * - Signs the action using EIP-712 * - Submits the signed action to the HyperCore API */ import { Wallet, Signature } from "ethers"; // -------- Configuration -------- const config = { privateKey: process.env.PRIVATE_KEY as string, // Transfer parameters amount: process.env.AMOUNT || "10", // 10 USDC (human-readable) sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot // Hyperliquid environment isMainnet: String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true", }; // System address for USDC token on HyperCore const USDC_SYSTEM_ADDRESS = "0x2000000000000000000000000000000000000000"; // -------- Main Function -------- async function main() { if (!config.privateKey) { throw new Error("Set PRIVATE_KEY"); } const apiUrl = config.isMainnet ? "https://api.hyperliquid.xyz" : "https://api.hyperliquid-testnet.xyz"; const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet"; const signingChainId = "0xa4b1"; // EVM chain ID for EIP-712 signing (any valid chain ID works) const chainId = parseInt(signingChainId, 16); const timestamp = Date.now(); const wallet = new Wallet(config.privateKey); console.log("Withdrawing from HyperCore to HyperEVM:", hyperliquidChain); console.log("User Address:", wallet.address); console.log("Source balance:", config.sourceDex || "perp"); console.log("Amount (USDC):", config.amount); // EIP-712 Domain const domain = { name: "HyperliquidSignTransaction", version: "1", chainId, verifyingContract: "0x0000000000000000000000000000000000000000", }; // Build action for signing const actionForSigning = { hyperliquidChain, signatureChainId: signingChainId, destination: USDC_SYSTEM_ADDRESS, sourceDex: config.sourceDex, destinationDex: "spot", token: "USDC", amount: config.amount, fromSubAccount: "", nonce: timestamp, }; // EIP-712 Types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES) const types = { "HyperliquidTransaction:SendAsset": [ { name: "hyperliquidChain", type: "string" }, { name: "destination", type: "string" }, { name: "sourceDex", type: "string" }, { name: "destinationDex", type: "string" }, { name: "token", type: "string" }, { name: "amount", type: "string" }, { name: "fromSubAccount", type: "string" }, { name: "nonce", type: "uint64" }, ], }; // Message to sign (only fields defined in EIP-712 types, not signatureChainId) const message = { hyperliquidChain: actionForSigning.hyperliquidChain, destination: actionForSigning.destination, sourceDex: actionForSigning.sourceDex, destinationDex: actionForSigning.destinationDex, token: actionForSigning.token, amount: actionForSigning.amount, fromSubAccount: actionForSigning.fromSubAccount, nonce: BigInt(actionForSigning.nonce), }; // Sign the message using EIP-712 const sigHex = await wallet.signTypedData(domain, types, message); const sig = Signature.from(sigHex); // Build action payload for API (includes type and signatureChainId) const action: any = { type: "sendAsset", ...actionForSigning, }; // Submit to Hyperliquid exchange API const response = await fetch(`${apiUrl}/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, nonce: timestamp, signature: { r: sig.r, s: sig.s, v: sig.v }, }), }); const result = await response.json(); console.log("\nStatus:", response.status); console.log("Response:", JSON.stringify(result, null, 2)); if (response.status === 200 && result.status === "ok") { console.log("\nWithdrawal initiated successfully"); } else { throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`); } } // Run main().catch((error) => { console.error("Error:", error.message); process.exit(1); }); ``` Run the script: ```bash theme={null} npx tsx script.ts ``` # Migrate from CCTP V1 (Legacy) to V2 Source: https://developers.circle.com/cctp/migration-from-v1-to-v2 Complete migration guide for developers upgrading CCTP integrations This guide provides a summary of the breaking changes when migrating from Cross-Chain Transfer Protocol (CCTP) V1 to V2. CCTP V2 introduces enhancements including Fast Transfer, Hooks features, and improved API endpoints, but requires updating your integration due to breaking changes. **Important**: CCTP V2 isn't backward compatible with V1. It uses separate contracts, APIs, and transfer speeds. It also introduces new blockchain support, while deprecating some chains. Plan for a complete integration update rather than incremental changes. Failure to migrate will eventually result in loss of crosschain capabilities for your integration. Arc App Kit's [Bridge](https://docs.arc.io/app-kit/bridge) capability can help simplify your migration to CCTP V2. See the [Migrating with App Kit](#migrating-with-app-kit) section for more information. ## V1 deprecation Circle is deprecating CCTP V1 to focus on the newer version, which is upgradable and provides a faster, more secure, and more robust crosschain experience across a wider network of blockchains. ### Naming changes CCTP V2 is now referred to as CCTP (except in this document). The V1 version of CCTP is now CCTP V1 (Legacy). ### Deprecation timeline CCTP V1 will be phased out over the course of 10 months beginning in July 2026. CCTP V2 contracts are available on all CCTP V1 chains except for Aptos, Noble, and Sui. Aptos and Sui will be supported by V2 before the phase out begins. Circle is working with Noble and Cosmos ecosystem teams on an intermediate solution to route USDC flows to and from Noble. ### Access to funds You will not lose access to funds during the V1 phase out. All pending redemptions will remain available as CCTP V1 (legacy) begins its phase out. Circle will maintain minter allowances greater than the total of pending attestations, ensuring every redemption can be processed before V1 contracts are fully paused. The deprecation process is designed to wind down activity gradually, message limits will tighten over time until no new burns can be initiated, bringing transfer volume to zero before contracts are fully paused. ### Additional resources In addition to this guide and [Arc App Kit](https://docs.arc.io/app-kit), you can contact the Circle team on the [BuildOnCircle Discord](https://discord.com/invite/buildoncircle) for questions and migration support. ## Summary of breaking changes The latest version of CCTP introduces architectural changes that make it incompatible with V1 integrations. You must update your implementation to use the new contracts, APIs, and transfer speeds. Additionally, the overall flow of the protocol has been streamlined, which means you need to update your integration to use the new functions. * Contracts are deployed at [different addresses](/cctp/references/contract-addresses) than V1 contracts. You should update your integration to point to the new contract addresses. * [Contract interfaces](/cctp/references/contract-interfaces) have changed. Importantly, the [`depositForBurn` function](/cctp/references/contract-interfaces#depositforburn) now takes additional parameters. You should update your integration to use the new ABIs and contract calls. * CCTP now allows you to specify a transfer speed. The `finalityThreshold` parameter specifies whether the transfer should be a [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) or a [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times). * You no longer need to extract the message from the onchain transaction to fetch an attestation. Instead, you can call the new `/v2/messages/{sourceDomainId}` endpoint with the transaction hash to get the message and attestation in a single call. * API endpoints have changed. The new `/v2/` endpoints have different functions than the old `/v1/` endpoints. You should update your integration to use the new endpoints. Review the [CCTP API reference](/api-reference/cctp/all/get-public-keys-v2) for details on the changes to the CCTP offchain API. * [Fees](/cctp/concepts/fees) have been introduced. Fast Transfer has a variable fee based on the source chain. You should update your integration to account for the new fees. ## Migrating with App Kit [Arc App Kit](https://docs.arc.io/app-kit) provides a simplified migration path by abstracting routine setup steps and standardizing bridging flows. This enables you to integrate bridging operations with minimal code. ### Benefits of using App Kit to bridge * **No contract management**: App Kit handles contract addresses, ABIs, and function calls for you. * **No attestation polling**: Automatically retrieves attestations without manual API calls. * **Built-in CCTP features**: Access Fast Transfer and other capabilities through simple configuration. * **Type-safe interface**: Compatible with `viem` and `ethers` for safer development. * **Fee collection**: Optionally collect fees from transfers to monetize your application. ### Example migration Replace manual contract calls and API polling with a single method: ```typescript theme={null} import { AppKit } from "@circle-fin/app-kit"; import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2"; // Initialize App Kit const kit = new AppKit(); // Create adapter for your wallet const adapter = createAdapterFromPrivateKey({ privateKey: process.env.PRIVATE_KEY as string, }); // Transfer USDC with Fast Transfer const result = await kit.bridge({ from: { adapter, chain: "Ethereum" }, to: { adapter, chain: "Base" }, amount: "100", config: { transferSpeed: "FAST", // Use Fast Transfer maxFee: "5000000", // Max 5 USDC fee (optional) }, }); // Result includes transaction details and explorer URLs console.log("Transfer complete:", result.steps); ``` For more information, see Arc App Kit's [Bridge](https://docs.arc.io/app-kit/bridge) capability. ## Changes to smart contracts CCTP uses new smart contracts with different names, addresses, and interfaces. You must update your integration to use the new contracts and their new function signatures. ### Contract name and address changes All legacy contracts have V2 equivalents deployed at new addresses: | Legacy contract | V2 contract | Documentation | | -------------------- | ---------------------- | --------------------------------------------------------------- | | `TokenMessenger` | `TokenMessengerV2` | [V2 Interface](/cctp/evm-smart-contracts#tokenmessengerv2) | | `MessageTransmitter` | `MessageTransmitterV2` | [V2 Interface](/cctp/evm-smart-contracts#messagetransmitterv2) | | `TokenMinter` | `TokenMinterV2` | [V2 Addresses](/cctp/evm-smart-contracts#tokenminterv2-mainnet) | | `Message` | `MessageV2` | [V2 Addresses](/cctp/evm-smart-contracts#messagev2-mainnet) | **Important**: V2 contracts are deployed at different addresses than V1 contracts. See the [CCTP Contract Addresses](/cctp/evm-smart-contracts#mainnet-contract-addresses) for the complete list of mainnet and testnet addresses. ### TokenMessengerV2 changes **Modified functions:** * `depositForBurn()` now requires three additional parameters: * `destinationCaller` (bytes32) - Address that can call `receiveMessage` on destination * `maxFee` (uint256) - Maximum fee for Fast Transfer in units of burn token * `minFinalityThreshold` (uint32) - Minimum finality level (1000 for Fast, 2000 for Standard) **New functions:** * `depositForBurnWithHook()` - Enables custom logic execution on destination chain via hook data * `getMinFeeAmount()` - Calculates minimum fee for Standard Transfer (on supported chains only) **Removed functions:** * `depositForBurnWithCaller()` - Use `destinationCaller` parameter in `depositForBurn()` instead * `replaceDepositForBurn()` - No V2 equivalent available ### Contract source code Full contract source code is available on GitHub: * [CCTP EVM Contracts](https://github.com/circlefin/evm-cctp-contracts) - Main repository * [Contract ABIs](https://github.com/circlefin/evm-cctp-contracts/tree/master/docs/abis/cctp/v2) - Interface definitions ## API migration guide CCTP streamlines the API workflow by combining message retrieval and attestation into single calls, while introducing new endpoints for features like Fast Transfer monitoring and re-attestation. ### Workflow changes The API eliminates the need to extract the message emitted by the onchain transaction: **Legacy workflow:** 1. Get the transaction receipt from the onchain transaction 2. Find the MessageSent event in the transaction receipt 3. Hash the message bytes emitted by the MessageSent event 4. Call `/v1/attestations/{messageHash}` to get an attestation **V2 workflow:** 1. Call `/v2/messages/{sourceDomainId}` with transaction hash or nonce to get message, attestation, and decoded data #### Legacy workflow example ```javascript theme={null} import { createPublicClient, http } from "viem"; import { sepolia } from "viem/chains"; // V1 requires multiple steps to extract message and get attestation const burnTxHash = "0x1234..."; // Transaction hash from depositForBurn // Step 1: Get the transaction receipt from the onchain transaction const client = createPublicClient({ chain: sepolia, transport: http(), }); const transactionReceipt = await client.getTransactionReceipt({ hash: burnTxHash, }); // Step 2: Find the MessageSent event in the transaction receipt const eventTopic = keccak256(toBytes("MessageSent(bytes)")); const log = transactionReceipt.logs.find((l) => l.topics[0] === eventTopic); const messageBytes = decodeAbiParameters([{ type: "bytes" }], log.data)[0]; // Step 3: Hash the message bytes emitted by the MessageSent event const messageHash = keccak256(messageBytes); // Step 4: Call attestation API with the message hash let attestationResponse = { status: "pending" }; while (attestationResponse.status !== "complete") { const response = await fetch( `https://iris-api-sandbox.circle.com/attestations/${messageHash}`, ); attestationResponse = await response.json(); await new Promise((r) => setTimeout(r, 2000)); } const attestation = attestationResponse.attestation; // Now you can use messageBytes and attestation to call receiveMessage ``` #### V2 workflow example ```javascript theme={null} // V2 gets message and attestation in a single call const sourceDomainId = 0; // Ethereum mainnet const transactionHash = "0x1234..."; // Single step: Get message, attestation, and decoded data const response = await fetch( `https://iris-api.circle.com/v2/messages/${sourceDomainId}?transactionHash=${transactionHash}`, ); const data = await response.json(); // All data available in single response const message = data.messages[0].message; const attestation = data.messages[0].attestation; const decodedMessage = data.messages[0].decodedMessage; // Now you can use message and attestation to call receiveMessage // You can also access decoded fields without manual parsing console.log(`Amount: ${decodedMessage.decodedMessageBody.amount}`); console.log(`Recipient: ${decodedMessage.decodedMessageBody.mintRecipient}`); ``` ### Endpoint migration mapping | Legacy endpoint | V2 replacement | Migration notes | | ----------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | | `GET /v1/attestations/{messageHash}` | `GET /v2/messages/{sourceDomainId}?transactionHash={hash}` | Combined into messages endpoint with enhanced response | | `GET /v1/messages/{sourceDomainId}/{transactionHash}` | `GET /v2/messages/{sourceDomainId}?transactionHash={hash}` | Enhanced with decoded data and attestation | | `GET /v1/publicKeys` | `GET /v2/publicKeys` | Multi-version support, backward compatible | ### New V2-only endpoints V2 introduces additional endpoints for advanced features: | Endpoint | Purpose | Use case | | -------------------------------------------------------- | --------------------------------- | ------------------------------------------------------ | | `POST /v2/reattest/{nonce}` | Re-attest messages for edge cases | Handle expired Fast Transfer burns or finality changes | | `GET /v2/fastBurn/USDC/allowance` | Monitor Fast Transfer allowance | Check remaining Fast Transfer capacity in real-time | | `GET /v2/burn/USDC/fees/{sourceDomainId}/{destDomainId}` | Get current transfer fees | Calculate fees before initiating transfers | ### Message data changes V2 message responses now include the decoded message data and attestation: #### V1 messages response ```json theme={null} { "messages": [ { "attestation": "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d4387756990c9c4b9280338325252866861f9495dce3128cd524d525c44e8e7b731dedd3098a618dcc19c45be1e1c", "message": "0x00000000000000050000000300000000000194c2...", "eventNonce": "9682" } ] } ``` #### V2 messages response ```json theme={null} { "messages": [ { "message": "0x00000000000000050000000300000000000194c2...", "eventNonce": "9682", "attestation": "0x6edd90f4a0ad0212fd9fbbd5058a25aa8ee10ce77e4fc143567bbe73fb6e164f384a3e14d350c8a4fc50b781177297e03c16b304e8d7656391df0f59a75a271f1b", "decodedMessage": { "sourceDomain": "7", "destinationDomain": "5", "nonce": "569", "sender": "0xca9142d0b9804ef5e239d3bc1c7aa0d1c74e7350", "recipient": "0xb7317b4EFEa194a22bEB42506065D3772C2E95EF", "destinationCaller": "0xf2Edb1Ad445C6abb1260049AcDDCA9E84D7D8aaA", "messageBody": "0x00000000000000050000000300000000000194c2...", "decodedMessageBody": { "burnToken": "0x4Bc078D75390C0f5CCc3e7f59Ae2159557C5eb85", "mintRecipient": "0xb7317b4EFEa194a22bEB42506065D3772C2E95EF", "amount": "5000", "messageSender": "0xca9142d0b9804ef5e239d3bc1c7aa0d1c74e7350" } }, "cctpVersion": 2, "status": "complete" } ] } ``` On Stellar, USDC precision and address encoding differ from other CCTP-supported blockchains. For inbound transfers, use [`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) so funds reach the correct recipient. See [CCTP on Stellar](/cctp/references/stellar). # Transfer USDC from Ethereum to Arc Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-ethereum-to-arc Build a script to transfer USDC between EVM blockchains using CCTP This guide demonstrates how to transfer USDC from Ethereum Sepolia to Arc testnet using CCTP. You use the [viem](https://viem.sh/) framework to interact with [CCTP contracts](/cctp/references/contract-addresses) and the [CCTP API](/api-reference/cctp/all/get-messages-v2) to retrieve attestations. **Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to simplify crosschain transfers with CCTP.** This quickstart shows how to transfer USDC from to using a manual CCTP integration. The example is for learning or for developers who need a manual integration. To streamline this, use Bridge Kit to transfer USDC in just a few lines of code. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM testnet wallet with the private key available * Added Arc testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your wallet with the following testnet tokens: * Sepolia ETH (native token) from a [public faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) * Sepolia USDC from the [Circle Faucet](https://faucet.circle.com) * Arc testnet USDC from the [Circle Faucet](https://faucet.circle.com) if you choose the direct mint path below, because the destination wallet must pay gas to call `receiveMessage` ## Step 1. Set up the project ### 1.1. Create the project and install dependencies ```shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-evm-transfer cd cctp-evm-transfer npm init -y # Set up module type and start command npm pkg set type=module npm pkg set scripts.start="tsx --env-file=.env index.ts" # Install runtime dependencies npm install viem # Install dev dependencies npm install --save-dev @types/node tsx typescript ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Set environment variables Open `.env` in your editor and add: ```text theme={null} PRIVATE_KEY=YOUR_ETHEREUM_SEPOLIA_PRIVATE_KEY ``` * `PRIVATE_KEY` is the private key for the Ethereum Sepolia EOA that signs the source-chain approval and burn transactions. The direct-mint path also uses the same key to submit the destination mint on Arc. 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. The `npm run start` command loads variables from `.env` using Node.js native env-file support. This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. ## Step 2: Configure the script This section covers the necessary setup for the transfer script, including defining keys and addresses, and configuring the wallet client for interacting with the source and destination chains. ### 2.1. Define configuration constants The script predefines the contract addresses, transfer amount, and maximum fee. Update the `DESTINATION_ADDRESS` with your wallet address. For simplicity, this quickstart uses the same EOA as the Ethereum Sepolia source signer and the Arc recipient. In production, these can be different addresses. ```ts TypeScript theme={null} // Authentication const PRIVATE_KEY = process.env.PRIVATE_KEY; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); // Contract Addresses const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; const ETHEREUM_SEPOLIA_TOKEN_MESSENGER = "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa"; const ARC_TESTNET_MESSAGE_TRANSMITTER = "0xe737e5cebeeba77efe34d4aa090756590b1ce275"; // Transfer Parameters const DESTINATION_ADDRESS = account.address; // Address to receive minted tokens on destination chain const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits) const maxFee = 500n; // 0.0005 USDC (500 subunits) // Bytes32 Formatted Parameters const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice( 2, )}`; // Destination address in bytes32 format const DESTINATION_CALLER_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage() // Chain-specific Parameters const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc testnet ``` ### 2.2. Set up wallet clients The wallet client configures the appropriate network settings using `viem`. The direct-mint path below uses clients for both Ethereum Sepolia and Arc testnet. The [Forwarding Service](/cctp/concepts/forwarding-service) path only needs the source-chain client on Ethereum Sepolia. ```ts TypeScript theme={null} // Set up the wallet clients const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); ``` ## Step 3: Implement the transfer logic The following sections outline the core transfer logic. The path diverges at the source-chain burn transaction: * **Direct mint** uses `depositForBurn`, then retrieves an attestation and calls `receiveMessage` on Arc. * **Forwarding Service** uses `depositForBurnWithHook`, then lets Circle handle the destination-side mint on Arc. ### 3.1. Get forwarding fees and calculate the burn amount Before you burn USDC with the Forwarding Service, query the CCTP fee endpoint with `forward=true`. The forwarding fee is dynamic, so fetch it immediately before the transfer. The returned `maxFee` must cover both the CCTP protocol fee and the forwarding fee. ```ts TypeScript theme={null} const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; async function getForwardingFees() { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${ETHEREUM_SEPOLIA_DOMAIN}/${ARC_TESTNET_DOMAIN}?forward=true`, { method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { throw new Error(`Failed to fetch fees: ${await response.text()}`); } return response.json(); } async function calculateForwardingAmounts() { const fees = await getForwardingFees(); const feeData = fees.find( (fee: { finalityThreshold: number }) => fee.finalityThreshold === 1000, ); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } const forwardFee = BigInt(feeData.forwardFee.med); const protocolFee = (AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = AMOUNT + maxFee; return { maxFee, totalAmount }; } ``` ### 3.2. Approve the total burn amount Approve the total amount you will burn on the source chain. For the forwarding path, that is the transfer amount plus the forwarding and protocol fees. ```ts TypeScript theme={null} async function approveUSDC(amount: bigint) { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, amount], }), }); console.log(`USDC Approval Tx: ${approveTx}`); } ``` ### 3.3. Burn USDC with the Forwarding Service hook Use `depositForBurnWithHook` on the source chain. The forwarding hook data tells Circle to handle the destination-side `receiveMessage` call on Arc. ```ts TypeScript theme={null} async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) { console.log("Burning USDC on Ethereum Sepolia with Forwarding Service..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ totalAmount, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } ``` ### 3.4. Verify the forwarded mint After the burn is confirmed, poll the Iris API until it returns a `forwardTxHash`. That hash is the Arc destination mint transaction submitted by Circle. In the forwarding path, `forwardTxHash` is the completion signal for the destination-side mint. You do not need to retrieve an attestation and call `receiveMessage` yourself. ```ts TypeScript theme={null} async function waitForForwardedMint(transactionHash: string) { console.log("Waiting for Forwarding Service to mint on Arc..."); while (true) { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`, { method: "GET" }, ); if (!response.ok) { await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = await response.json(); const forwardTxHash = data?.messages?.[0]?.forwardTxHash; if (forwardTxHash) { console.log(`Forwarded Mint Tx: ${forwardTxHash}`); return forwardTxHash; } await new Promise((resolve) => setTimeout(resolve, 5000)); } } ``` ### 3.1. Approve USDC Grant approval for the [`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on Ethereum Sepolia to withdraw USDC from your wallet. This allows the contract to burn USDC when you initiate the transfer. ```ts TypeScript theme={null} async function approveUSDC() { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000n], // 10 USDC allowance }), }); console.log(`USDC Approval Tx: ${approveTx}`); } ``` ### 3.2. Burn USDC Call the `depositForBurn` function from the [`TokenMessengerV2` contract](/cctp/references/contract-interfaces#depositforburn) deployed on Ethereum Sepolia to burn USDC on that source chain. You specify the following parameters: * **Burn amount**: The amount of USDC to burn * **Destination domain**: The target blockchain for minting USDC (see [supported chains and domains](/cctp/concepts/supported-chains-and-domains)) * **Mint recipient**: The wallet address that will receive the minted USDC * **Burn token**: The contract address of the USDC token being burned on the source chain * **Destination caller**: The address on the target chain to call `receiveMessage` * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer * **Finality threshold**: Determines whether it's a [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) (1000 or less) or a [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times) (2000 or more) ```ts TypeScript theme={null} async function burnUSDC() { console.log("Burning USDC on Ethereum Sepolia..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurn", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, ], outputs: [], }, ], functionName: "depositForBurn", args: [ AMOUNT, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, // minFinalityThreshold (1000 or less for Fast Transfer) ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } ``` ### 3.3. Retrieve attestation Retrieve the attestation required to complete the CCTP transfer by calling Circle's attestation API. * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) API endpoint to retrieve the attestation. * Pass the `srcDomain` argument from the [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) for your source chain. * Pass `transactionHash` from the value returned by `sendTransaction` within the `burnUSDC` function above. ```ts TypeScript theme={null} async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } ``` ### 3.4. Mint USDC Call the [`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage) from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses) deployed on the Arc testnet to mint USDC on that destination chain. * Pass the signed attestation and the message data as parameters. * The function processes the attestation and mints USDC to the specified Arc testnet wallet address. ```ts TypeScript theme={null} async function mintUSDC(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_TESTNET_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint Tx: ${mintTx}`); } ``` ## Step 4: Complete script Create a `index.ts` file in your project directory and populate it with the complete code below for the path you want to test. ```ts index.ts expandable theme={null} import { createPublicClient, createWalletClient, http, encodeFunctionData, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { sepolia } from "viem/chains"; type FeeQuote = { finalityThreshold: number; minimumFee: number; forwardFee: { med: number }; }; const PRIVATE_KEY = process.env.PRIVATE_KEY; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; const ETHEREUM_SEPOLIA_TOKEN_MESSENGER = "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa"; const DESTINATION_ADDRESS = account.address; const AMOUNT = 1_000_000n; const ETHEREUM_SEPOLIA_DOMAIN = 0; const ARC_TESTNET_DOMAIN = 26; const DESTINATION_ADDRESS_BYTES32 = pad(DESTINATION_ADDRESS, { size: 32 }); const DESTINATION_CALLER_BYTES32 = pad("0x", { size: 32 }); const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const sepoliaPublicClient = createPublicClient({ chain: sepolia, transport: http(), }); async function approveUSDC(amount: bigint) { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, amount], }), }); console.log(`USDC Approval Tx: ${approveTx}`); await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx }); } async function getForwardingFeeQuote() { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${ETHEREUM_SEPOLIA_DOMAIN}/${ARC_TESTNET_DOMAIN}?forward=true`, { method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { throw new Error(`Failed to fetch fees: ${await response.text()}`); } const fees = (await response.json()) as FeeQuote[]; const feeData = fees.find((fee) => fee.finalityThreshold === 1000); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } return feeData; } async function calculateForwardingAmounts() { const feeData = await getForwardingFeeQuote(); const forwardFee = BigInt(feeData.forwardFee.med); const protocolFee = (AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = AMOUNT + maxFee; console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); return { maxFee, totalAmount }; } async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) { console.log("Burning USDC on Ethereum Sepolia with Forwarding Service..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ totalAmount, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } async function waitForForwardedMint(transactionHash: string) { console.log("Waiting for Forwarding Service to mint on Arc..."); while (true) { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`, { method: "GET" }, ); if (!response.ok) { await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = await response.json(); const forwardTxHash = data?.messages?.[0]?.forwardTxHash; if (forwardTxHash) { console.log(`Forwarded Mint Tx: ${forwardTxHash}`); return forwardTxHash; } await new Promise((resolve) => setTimeout(resolve, 5000)); } } async function main() { console.log("Wallet address:", account.address); // [1] Quote forwarding fees and derive the total source-chain burn amount. const { maxFee, totalAmount } = await calculateForwardingAmounts(); // [2] Approve the total burn amount, including forwarding and protocol fees. await approveUSDC(totalAmount); // [3] Burn on the source chain with the forwarding hook enabled. const burnTx = await burnUSDCWithForwarding(totalAmount, maxFee); // [4] Poll until Iris returns the destination mint transaction hash. await waitForForwardedMint(burnTx); console.log("USDC transfer completed with Forwarding Service."); } main().catch(console.error); ``` ```ts index.ts expandable theme={null} import { createPublicClient, createWalletClient, http, encodeFunctionData, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet, sepolia } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Authentication const PRIVATE_KEY = process.env.PRIVATE_KEY; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); // Contract Addresses const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; const ETHEREUM_SEPOLIA_TOKEN_MESSENGER = "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa"; const ARC_TESTNET_MESSAGE_TRANSMITTER = "0xe737e5cebeeba77efe34d4aa090756590b1ce275"; // Transfer Parameters const DESTINATION_ADDRESS = account.address; // Address to receive minted tokens on destination chain const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits) const maxFee = 500n; // 0.0005 USDC (500 subunits) // Bytes32 Formatted Parameters const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice( 2, )}`; // Destination address in bytes32 format const DESTINATION_CALLER_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage() // Chain-specific Parameters const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc testnet // Set up wallet clients const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const sepoliaPublicClient = createPublicClient({ chain: sepolia, transport: http(), }); const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); // ============ CCTP Flow Functions ============ async function approveUSDC() { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_USDC, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000n], // 10 USDC allowance }), }); console.log(`USDC Approval Tx: ${approveTx}`); await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx }); } async function burnUSDC() { console.log("Burning USDC on Ethereum Sepolia..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurn", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, ], outputs: [], }, ], functionName: "depositForBurn", args: [ AMOUNT, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32 as `0x${string}`, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, // minFinalityThreshold (1000 or less for Fast Transfer) ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } async function mintUSDC(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_TESTNET_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint Tx: ${mintTx}`); } // ============ Main Execution ============ async function main() { await approveUSDC(); const burnTx = await burnUSDC(); const attestation = await retrieveAttestation(burnTx); await mintUSDC(attestation); console.log("USDC transfer completed."); } main().catch(console.error); ``` ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` Once the script runs and the transfer is finalized, a confirmation receipt is logged in the console. **Rate limit:** The attestation service rate limit is 35 requests per second. If you exceed this limit, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 (Too Many Requests) response. # Transfer USDC from Solana to Arc Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-solana-to-arc Transfer USDC from Solana to an EVM blockchain using CCTP This guide demonstrates how to transfer USDC from Solana Devnet to Arc Testnet using CCTP. You use the [Solana Kit](https://github.com/anza-xyz/kit) library to interact with [Solana CCTP programs](/cctp/references/solana-programs), and viem to mint USDC on Arc Testnet. **Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to simplify crosschain transfers with CCTP.** This quickstart shows how to transfer USDC from to using a manual CCTP integration. The example is for learning or for developers who need a manual integration. To streamline this, use Bridge Kit to transfer USDC in just a few lines of code. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared a Solana wallet and have the private key array available * Funded your Solana wallet with the following testnet tokens: * Solana Devnet SOL (native token) from a [public faucet](https://faucet.solana.com/) * Solana Devnet USDC from the [Circle Faucet](https://faucet.circle.com) * Prepared an EVM testnet wallet with the private key available * Added Arc testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your EVM wallet with Arc Testnet USDC from the [Circle Faucet](https://faucet.circle.com) if you choose the direct mint path below, because the destination wallet must pay gas to call `receiveMessage` ## Step 1. Set up the project ### 1.1. Create the project and install dependencies ```shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-solana-transfer cd cctp-solana-transfer npm init -y # Set up module type and start command npm pkg set type=module npm pkg set scripts.start="tsx --env-file=.env index.ts" # Install runtime dependencies npm install @solana/kit @solana-program/system @solana-program/token viem # Install dev dependencies npm install --save-dev @types/node tsx typescript ``` ### 1.2. Configure TypeScript (optional) This step is optional. It helps prevent missing types in your IDE or editor. Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Set environment variables Open `.env` in your editor and add: ```text theme={null} SOLANA_PRIVATE_KEY=YOUR_SOLANA_PRIVATE_KEY_ARRAY EVM_PRIVATE_KEY=YOUR_ARC_PRIVATE_KEY ``` * `SOLANA_PRIVATE_KEY` is the private key array for the Solana Devnet wallet that signs the source-chain burn transaction. * `EVM_PRIVATE_KEY` is used to derive the Arc recipient address. The direct-mint path also uses this key to submit the destination mint on Arc. 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. The `npm run start` command loads variables from `.env` using Node.js native env-file support. This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. ## Step 2: Configure the script Define the configuration constants for interacting with Solana and Arc Testnet. ### 2.1. Setup chains and wallets The script predefines the program addresses, transfer amount, and other parameters: ```ts TypeScript expandable theme={null} // Solana Configuration const SOLANA_RPC = "https://api.devnet.solana.com"; const SOLANA_WS = "wss://api.devnet.solana.com"; const rpc = createSolanaRpc(SOLANA_RPC); const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS); const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!); const solanaKeypair = await createKeyPairSignerFromBytes( Uint8Array.from(solanaPrivateKey), ); // Solana CCTP Program Addresses (Devnet) const TOKEN_MESSENGER_MINTER_PROGRAM = address( "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", ); const MESSAGE_TRANSMITTER_PROGRAM = address( "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", ); const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); const ASSOCIATED_TOKEN_PROGRAM = address( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", ); // Arc Testnet Configuration const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!; const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`); const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account: ethAccount, }); // Transfer Parameters const AMOUNT = 1_000_000n; const DESTINATION_DOMAIN = 26; const ARC_DESTINATION_ADDRESS = ethAccount.address; const MAX_FEE = 500n; ``` ## Step 3: Implement the transfer logic The following sections outline the core transfer logic from Solana to Arc. For simplicity, this quickstart uses the same Arc wallet as the recipient and, in the direct-mint path, the wallet that submits `receiveMessage`. In production, these can be different addresses. In the two examples provided, the path diverges at the Solana burn instruction: * **Direct mint** uses `deposit_for_burn`, then retrieves an attestation and calls `receiveMessage` on Arc. * **Forwarding Service** uses `deposit_for_burn_with_hook`, then lets Circle handle the destination-side mint on Arc. ### 3.1. Get forwarding fees and calculate the burn amount Before you burn USDC with the [Forwarding Service](/cctp/concepts/forwarding-service), query the CCTP fee endpoint with `forward=true`. The forwarding fee is dynamic, so fetch it immediately before the transfer. The returned `maxFee` must cover both the CCTP protocol fee and the forwarding fee. ```ts TypeScript theme={null} const FORWARDING_SERVICE_HOOK_DATA = Buffer.from( "636374702d666f72776172640000000000000000000000000000000000000000", "hex", ); async function getForwardingFees() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/26?forward=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { throw new Error(`Failed to fetch fees: ${await response.text()}`); } return response.json(); } async function calculateForwardingAmounts() { const fees = await getForwardingFees(); const feeData = fees.find( (fee: { finalityThreshold: number }) => fee.finalityThreshold === 1000, ); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } const forwardFee = BigInt(feeData.forwardFee.med); const protocolFee = (AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = AMOUNT + maxFee; return { maxFee, totalAmount }; } ``` ### 3.2. Burn USDC with the Forwarding Service hook Use `deposit_for_burn_with_hook` on Solana. The forwarding hook data tells Circle to handle the destination-side `receiveMessage` call on Arc. ```ts TypeScript expandable theme={null} type BurnContext = { senderUsdcAccount: ReturnType; senderAuthorityPda: ReturnType; denylistPda: ReturnType; messageTransmitter: ReturnType; tokenMessenger: ReturnType; remoteTokenMessenger: ReturnType; tokenMinter: ReturnType; localToken: ReturnType; eventAuthority: ReturnType; messageTransmitterEventAuthority: ReturnType; messageSentEventAccount: Awaited>; destAddressBytes32: Buffer; }; async function getBurnContext(): Promise { const addressEncoder = getAddressEncoder(); const [senderUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); const destAddressBytes32 = Buffer.concat([ Buffer.alloc(12), Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"), ]); const [senderAuthorityPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("sender_authority")], }); const [denylistPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("denylist_account"), addressEncoder.encode(solanaKeypair.address), ], }); const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(DESTINATION_DOMAIN.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); const [eventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const messageSentEventAccount = await generateKeyPairSigner(); return { senderUsdcAccount, senderAuthorityPda, denylistPda, messageTransmitter, tokenMessenger, remoteTokenMessenger, tokenMinter, localToken, eventAuthority, messageTransmitterEventAuthority, messageSentEventAccount, destAddressBytes32, }; } async function burnUSDCWithForwarding() { console.log("Burning USDC on Solana with Forwarding Service..."); const { maxFee, totalAmount } = await calculateForwardingAmounts(); const burnContext = await getBurnContext(); const amountBuffer = Buffer.alloc(8); amountBuffer.writeBigUInt64LE(totalAmount); const domainBuffer = Buffer.alloc(4); domainBuffer.writeUInt32LE(DESTINATION_DOMAIN); const maxFeeBuffer = Buffer.alloc(8); maxFeeBuffer.writeBigUInt64LE(maxFee); const finalityBuffer = Buffer.alloc(4); finalityBuffer.writeUInt32LE(1000); const hookLengthBuffer = Buffer.alloc(4); hookLengthBuffer.writeUInt32LE(FORWARDING_SERVICE_HOOK_DATA.length); const instructionData = new Uint8Array( Buffer.concat([ Buffer.from([111, 245, 62, 131, 204, 108, 223, 155]), amountBuffer, domainBuffer, burnContext.destAddressBytes32, Buffer.alloc(32), maxFeeBuffer, finalityBuffer, hookLengthBuffer, FORWARDING_SERVICE_HOOK_DATA, ]), ); const depositForBurnIx = { programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: burnContext.senderAuthorityPda, role: 0 }, { address: burnContext.senderUsdcAccount, role: 1 }, { address: burnContext.denylistPda, role: 0 }, { address: burnContext.messageTransmitter, role: 1 }, { address: burnContext.tokenMessenger, role: 0 }, { address: burnContext.remoteTokenMessenger, role: 0 }, { address: burnContext.tokenMinter, role: 0 }, { address: burnContext.localToken, role: 1 }, { address: USDC_MINT, role: 1 }, { address: burnContext.messageSentEventAccount.address, role: 3, signer: burnContext.messageSentEventAccount, }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: burnContext.eventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: burnContext.messageTransmitterEventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); await sendAndConfirmTransaction(signedTransaction as any, { commitment: "confirmed", }); const signature = getSignatureFromTransaction(signedTransaction); console.log(`Burn transaction signature: ${signature}`); return signature; } ``` ### 3.3. Verify the forwarded mint After the burn is confirmed, poll the Iris API until it returns a `forwardTxHash`. That hash is the Arc destination mint transaction submitted by Circle. In the forwarding path, `forwardTxHash` is the completion signal for the destination-side mint. You do not need to retrieve an attestation and call `receiveMessage` yourself. ```ts TypeScript theme={null} async function waitForForwardedMint(transactionSignature: string) { console.log("Waiting for Forwarding Service to mint on Arc..."); while (true) { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`, { method: "GET" }, ); if (!response.ok) { await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = await response.json(); const forwardTxHash = data?.messages?.[0]?.forwardTxHash; if (forwardTxHash) { console.log(`Forwarded Mint Tx: ${forwardTxHash}`); return forwardTxHash; } await new Promise((resolve) => setTimeout(resolve, 5000)); } } ``` ### 3.1. Burn USDC on Solana Call the `depositForBurn` instruction from the `TokenMessengerMinterV2` program to burn USDC on Solana: ```ts TypeScript expandable theme={null} const DIRECT_MINT_DISCRIMINATOR = Buffer.from([ 215, 60, 61, 46, 114, 55, 128, 176, ]); async function burnUSDCOnSolana() { console.log("Burning USDC on Solana..."); const addressEncoder = getAddressEncoder(); // Get the sender's USDC token account (Associated Token Account PDA) const [senderUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); const destAddressBytes32 = Buffer.concat([ Buffer.alloc(12), Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"), ]); // Derive PDAs (Program Derived Addresses) const [senderAuthorityPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("sender_authority")], }); const [denylistPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("denylist_account"), addressEncoder.encode(solanaKeypair.address), ], }); const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); // NOTE: Domain is converted to string for PDA derivation in V2 const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(DESTINATION_DOMAIN.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); // Derive event authority PDAs for Anchor CPI events const [eventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const messageSentEventAccount = await generateKeyPairSigner(); const amountBuffer = Buffer.alloc(8); amountBuffer.writeBigUInt64LE(AMOUNT); const domainBuffer = Buffer.alloc(4); domainBuffer.writeUInt32LE(DESTINATION_DOMAIN); const maxFeeBuffer = Buffer.alloc(8); maxFeeBuffer.writeBigUInt64LE(MAX_FEE); const finalityBuffer = Buffer.alloc(4); finalityBuffer.writeUInt32LE(1000); const instructionData = new Uint8Array( Buffer.concat([ DIRECT_MINT_DISCRIMINATOR, amountBuffer, domainBuffer, destAddressBytes32, Buffer.alloc(32), maxFeeBuffer, finalityBuffer, ]), ); const depositForBurnIx = { programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: senderAuthorityPda, role: 0 }, { address: senderUsdcAccount, role: 1 }, { address: denylistPda, role: 0 }, { address: messageTransmitter, role: 1 }, { address: tokenMessenger, role: 0 }, { address: remoteTokenMessenger, role: 0 }, { address: tokenMinter, role: 0 }, { address: localToken, role: 1 }, { address: USDC_MINT, role: 1 }, { address: messageSentEventAccount.address, role: 3, signer: messageSentEventAccount, }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: eventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: messageTransmitterEventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); await sendAndConfirmTransaction(signedTransaction as any, { commitment: "confirmed", }); const signature = getSignatureFromTransaction(signedTransaction); console.log(`Burn transaction signature: ${signature}`); return signature; } ``` ### 3.2. Retrieve attestation Retrieve the attestation required to complete the CCTP transfer by calling Circle's attestation API: ```ts TypeScript theme={null} async function retrieveAttestation(transactionSignature: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } ``` ### 3.3. Mint USDC on Arc Testnet Call the `receiveMessage` function from the `MessageTransmitterV2` contract on Arc Testnet to mint USDC: ```ts TypeScript theme={null} async function mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint transaction hash: ${mintTx}`); } ``` ## Step 4: Complete script Create a `index.ts` file in your project directory and populate it with the complete code below for the path you want to test. ```ts index.ts expandable theme={null} import { address, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, generateKeyPairSigner, getAddressEncoder, getProgramDerivedAddress, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, signTransactionMessageWithSigners, } from "@solana/kit"; import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system"; import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { privateKeyToAccount } from "viem/accounts"; type FeeQuote = { finalityThreshold: number; minimumFee: number; forwardFee: { med: number }; }; type BurnContext = { senderUsdcAccount: ReturnType; senderAuthorityPda: ReturnType; denylistPda: ReturnType; messageTransmitter: ReturnType; tokenMessenger: ReturnType; remoteTokenMessenger: ReturnType; tokenMinter: ReturnType; localToken: ReturnType; eventAuthority: ReturnType; messageTransmitterEventAuthority: ReturnType; messageSentEventAccount: Awaited>; destAddressBytes32: Buffer; }; const SOLANA_RPC = "https://api.devnet.solana.com"; const SOLANA_WS = "wss://api.devnet.solana.com"; const rpc = createSolanaRpc(SOLANA_RPC); const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS); const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!); const solanaKeypair = await createKeyPairSignerFromBytes( Uint8Array.from(solanaPrivateKey), ); const TOKEN_MESSENGER_MINTER_PROGRAM = address( "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", ); const MESSAGE_TRANSMITTER_PROGRAM = address( "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", ); const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); const ASSOCIATED_TOKEN_PROGRAM = address( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", ); const ethAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY! as `0x${string}`, ); const AMOUNT = 1_000_000n; const DESTINATION_DOMAIN = 26; const ARC_DESTINATION_ADDRESS = ethAccount.address; const FORWARDING_SERVICE_HOOK_DATA = Buffer.from( "636374702d666f72776172640000000000000000000000000000000000000000", "hex", ); const FORWARDING_DISCRIMINATOR = Buffer.from([ 111, 245, 62, 131, 204, 108, 223, 155, ]); async function getForwardingFeeQuote() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/26?forward=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { throw new Error(`Failed to fetch fees: ${await response.text()}`); } const fees = (await response.json()) as FeeQuote[]; const feeData = fees.find((fee) => fee.finalityThreshold === 1000); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } return feeData; } async function calculateForwardingAmounts() { const feeData = await getForwardingFeeQuote(); const forwardFee = BigInt(feeData.forwardFee.med); const protocolFee = (AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = AMOUNT + maxFee; console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); return { maxFee, totalAmount }; } async function getBurnContext(): Promise { const addressEncoder = getAddressEncoder(); const [senderUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); const destAddressBytes32 = Buffer.concat([ Buffer.alloc(12), Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"), ]); const [senderAuthorityPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("sender_authority")], }); const [denylistPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("denylist_account"), addressEncoder.encode(solanaKeypair.address), ], }); const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(DESTINATION_DOMAIN.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); const [eventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const messageSentEventAccount = await generateKeyPairSigner(); return { senderUsdcAccount, senderAuthorityPda, denylistPda, messageTransmitter, tokenMessenger, remoteTokenMessenger, tokenMinter, localToken, eventAuthority, messageTransmitterEventAuthority, messageSentEventAccount, destAddressBytes32, }; } async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) { console.log("Burning USDC on Solana with Forwarding Service..."); const burnContext = await getBurnContext(); const amountBuffer = Buffer.alloc(8); amountBuffer.writeBigUInt64LE(totalAmount); const domainBuffer = Buffer.alloc(4); domainBuffer.writeUInt32LE(DESTINATION_DOMAIN); const maxFeeBuffer = Buffer.alloc(8); maxFeeBuffer.writeBigUInt64LE(maxFee); const finalityBuffer = Buffer.alloc(4); finalityBuffer.writeUInt32LE(1000); const hookLengthBuffer = Buffer.alloc(4); hookLengthBuffer.writeUInt32LE(FORWARDING_SERVICE_HOOK_DATA.length); const instructionData = new Uint8Array( Buffer.concat([ FORWARDING_DISCRIMINATOR, amountBuffer, domainBuffer, burnContext.destAddressBytes32, Buffer.alloc(32), maxFeeBuffer, finalityBuffer, hookLengthBuffer, FORWARDING_SERVICE_HOOK_DATA, ]), ); const depositForBurnIx = { programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: burnContext.senderAuthorityPda, role: 0 }, { address: burnContext.senderUsdcAccount, role: 1 }, { address: burnContext.denylistPda, role: 0 }, { address: burnContext.messageTransmitter, role: 1 }, { address: burnContext.tokenMessenger, role: 0 }, { address: burnContext.remoteTokenMessenger, role: 0 }, { address: burnContext.tokenMinter, role: 0 }, { address: burnContext.localToken, role: 1 }, { address: USDC_MINT, role: 1 }, { address: burnContext.messageSentEventAccount.address, role: 3, signer: burnContext.messageSentEventAccount, }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: burnContext.eventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: burnContext.messageTransmitterEventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); await sendAndConfirmTransaction(signedTransaction as any, { commitment: "confirmed", }); const signature = getSignatureFromTransaction(signedTransaction); console.log(`Burn transaction signature: ${signature}`); return signature; } async function waitForForwardedMint(transactionSignature: string) { console.log("Waiting for Forwarding Service to mint on Arc..."); while (true) { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`, { method: "GET" }, ); if (!response.ok) { await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = await response.json(); const forwardTxHash = data?.messages?.[0]?.forwardTxHash; if (forwardTxHash) { console.log(`Forwarded Mint Tx: ${forwardTxHash}`); return forwardTxHash; } await new Promise((resolve) => setTimeout(resolve, 5000)); } } async function main() { console.log("Solana address:", solanaKeypair.address); console.log("Arc recipient:", ARC_DESTINATION_ADDRESS); // [1] Quote forwarding fees and derive the total source-chain burn amount. const { maxFee, totalAmount } = await calculateForwardingAmounts(); // [2] Burn on Solana with the forwarding hook enabled. const burnSignature = await burnUSDCWithForwarding(totalAmount, maxFee); // [3] Poll until Iris returns the destination mint transaction hash. await waitForForwardedMint(burnSignature); console.log( "USDC transfer from Solana Devnet to Arc Testnet completed with Forwarding Service.", ); } main().catch(console.error); ``` ```ts index.ts expandable theme={null} import { address, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, generateKeyPairSigner, getAddressEncoder, getProgramDerivedAddress, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, signTransactionMessageWithSigners, } from "@solana/kit"; import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system"; import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; import { createWalletClient, http, encodeFunctionData } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } const SOLANA_RPC = "https://api.devnet.solana.com"; const SOLANA_WS = "wss://api.devnet.solana.com"; const rpc = createSolanaRpc(SOLANA_RPC); const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS); const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!); const solanaKeypair = await createKeyPairSignerFromBytes( Uint8Array.from(solanaPrivateKey), ); // Solana CCTP Program Addresses (Devnet) const TOKEN_MESSENGER_MINTER_PROGRAM = address( "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", ); const MESSAGE_TRANSMITTER_PROGRAM = address( "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", ); const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); const ASSOCIATED_TOKEN_PROGRAM = address( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", ); const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!; const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`); const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account: ethAccount, }); const AMOUNT = 1_000_000n; const DESTINATION_DOMAIN = 26; const ARC_DESTINATION_ADDRESS = ethAccount.address; const MAX_FEE = 500n; const DIRECT_MINT_DISCRIMINATOR = Buffer.from([ 215, 60, 61, 46, 114, 55, 128, 176, ]); async function burnUSDCOnSolana() { console.log("Burning USDC on Solana..."); const addressEncoder = getAddressEncoder(); const [senderUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); const destAddressBytes32 = Buffer.concat([ Buffer.alloc(12), Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"), ]); const [senderAuthorityPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("sender_authority")], }); const [denylistPda] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("denylist_account"), addressEncoder.encode(solanaKeypair.address), ], }); const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(DESTINATION_DOMAIN.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); const [eventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const messageSentEventAccount = await generateKeyPairSigner(); const amountBuffer = Buffer.alloc(8); amountBuffer.writeBigUInt64LE(AMOUNT); const domainBuffer = Buffer.alloc(4); domainBuffer.writeUInt32LE(DESTINATION_DOMAIN); const maxFeeBuffer = Buffer.alloc(8); maxFeeBuffer.writeBigUInt64LE(MAX_FEE); const finalityBuffer = Buffer.alloc(4); finalityBuffer.writeUInt32LE(1000); const instructionData = new Uint8Array( Buffer.concat([ DIRECT_MINT_DISCRIMINATOR, amountBuffer, domainBuffer, destAddressBytes32, Buffer.alloc(32), maxFeeBuffer, finalityBuffer, ]), ); const depositForBurnIx = { programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: senderAuthorityPda, role: 0 }, { address: senderUsdcAccount, role: 1 }, { address: denylistPda, role: 0 }, { address: messageTransmitter, role: 1 }, { address: tokenMessenger, role: 0 }, { address: remoteTokenMessenger, role: 0 }, { address: tokenMinter, role: 0 }, { address: localToken, role: 1 }, { address: USDC_MINT, role: 1 }, { address: messageSentEventAccount.address, role: 3, signer: messageSentEventAccount, }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: eventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: messageTransmitterEventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); await sendAndConfirmTransaction(signedTransaction as any, { commitment: "confirmed", }); const signature = getSignatureFromTransaction(signedTransaction); console.log(`Burn transaction signature: ${signature}`); return signature; } async function retrieveAttestation(transactionSignature: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } async function mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint transaction hash: ${mintTx}`); } async function main() { // [1] Burn USDC on Solana Devnet. const burnSignature = await burnUSDCOnSolana(); // [2] Poll until Iris returns a complete attestation. const attestation = await retrieveAttestation(burnSignature); // [3] Submit the destination-side mint on Arc Testnet. await mintUSDCOnArc(attestation); console.log("USDC transfer from Solana Devnet to Arc Testnet completed."); } main().catch(console.error); ``` ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` Once the script runs and the transfer is finalized, a confirmation message is logged in the console. **Rate limit:** The attestation service rate limit is 35 requests per second. If you exceed this limit, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 (Too Many Requests) response. # Transfer USDC to and from Stellar Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc Build scripts to transfer USDC between Arc Testnet and Stellar Testnet using CCTP Use CCTP to transfer USDC with Stellar Testnet as the source or destination. On Stellar, USDC precision and address encoding differ from other CCTP-supported blockchains. Before you integrate beyond these examples, read [CCTP on Stellar](/cctp/references/stellar). Pick the tab that matches the direction of your transfer. This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc Testnet using CCTP. You use the [@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) library to interact with Stellar Soroban contracts, and [`viem`](https://viem.sh/) to mint USDC on Arc Testnet. When you finish, you will have executed a full burn-attest-mint flow. You should be comfortable using a terminal and Node.js. Familiarity with Stellar Soroban transactions and basic EVM usage helps you follow and adapt the script. Examples use Arc Testnet as the destination, but you can use any [supported blockchain](/cctp/concepts/supported-chains-and-domains). ## Prerequisites Before you begin this tutorial, ensure you have: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM wallet with the private key available for Arc Testnet * Added the Arc Testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your wallet with Arc Testnet USDC (for gas fees) from the [Circle Faucet](https://faucet.circle.com) * Prepared a Stellar Testnet wallet with the secret key (`S...`) available * Funded your Stellar wallet with testnet XLM from the [Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees on Stellar Testnet) * Established a [USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) on your Stellar account so you can hold the testnet USDC you burn * Funded your Stellar wallet with Stellar Testnet USDC from the [Circle Faucet](https://faucet.circle.com) You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund accounts and establish USDC trustlines. ## Step 1: Set up the project This step shows you how to prepare your project and environment. ### 1.1. Set up your development environment Create a new directory and install the required dependencies: ```bash Shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-stellar-to-arc cd cctp-stellar-to-arc npm init -y # Set up module type and start command npm pkg set type=module npm pkg set scripts.start="npx tsx --env-file=.env index.ts" # Install runtime dependencies npm install @stellar/stellar-sdk viem # Install dev dependencies npm install --save-dev typescript @types/node ``` ### 1.2. Initialize and configure the project This step is optional. It helps prevent missing types in your IDE or editor. Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Configure environment variables Open .env in your editor and add: ```text theme={null} STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY ``` * `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban transactions on Stellar Testnet. * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc Testnet. 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. The `npm run start` command loads variables from `.env` using Node.js native env-file support. This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. ## Step 2: Configure the script Define contract addresses, amounts, and clients for Stellar Testnet and Arc Testnet. ### 2.1. Define configuration constants The script predefines the contract addresses, transfer amount, and maximum fee. ```ts TypeScript theme={null} import { Address, Contract, Keypair, nativeToScVal, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // Contract Addresses const STELLAR_TOKEN_MESSENGER_MINTER = "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP"; const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; // Transfer Parameters const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals) const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals) // Chain-specific Parameters const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; // Authentication const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); ``` ### 2.2. Set up wallet clients The wallet client configures the appropriate network settings using `viem`. In this example, the script connects to Arc Testnet. ```ts TypeScript theme={null} const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); ``` ### 2.3. Add helper function The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban contract transaction. ```ts TypeScript theme={null} async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } ``` ## Step 3: Implement the transfer logic This step implements the core transfer logic: approve and burn on Stellar, poll for an attestation, then mint on Arc. A successful run prints transaction hashes and a completion message in the console. ### 3.1. Approve USDC on Stellar Approve the `TokenMessengerMinterV2` contract to spend your USDC. The `submitSorobanTx` helper is used to submit the approve call to the Stellar USDC contract. ```ts TypeScript theme={null} async function approveUSDC() { console.log("Approving USDC spend on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const latestLedger = await server.getLatestLedger(); const expirationLedger = latestLedger.sequence + 100_000; const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [ new Address(stellarKeypair.publicKey()).toScVal(), new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(expirationLedger, { type: "u32" }), ]); console.log(`Approve Tx: ${approveHash}`); } ``` ### 3.2. Burn USDC on Stellar Call `deposit_for_burn` to burn USDC on Stellar. The `submitSorobanTx` helper is used to submit the burn call with the transfer parameters listed below: * **Burn amount**: The amount of USDC to burn (in Stellar subunits, 7 decimals) * **Destination domain**: The target blockchain for minting USDC (see [supported blockchains and domains](/cctp/concepts/supported-chains-and-domains)) * **Mint recipient**: The wallet address that receives the minted USDC on Arc * **Burn token**: The contract address of the USDC token on Stellar * **Destination caller**: The address on the target blockchain that may call `receiveMessage` * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer (in Stellar subunits, 7 decimals) * **Finality threshold**: Determines whether it's a [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) (1000 or less) or a [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times) (2000 or more) ```ts TypeScript theme={null} async function burnUSDC() { console.log("Burning USDC on Stellar..."); // Bytes32 Formatted Parameters const evmAddressBytes32 = pad(evmAccount.address); const mintRecipient = xdr.ScVal.scvBytes( Buffer.from(evmAddressBytes32.slice(2), "hex"), ); const server = new rpc.Server(STELLAR_RPC_URL); const txHash = await submitSorobanTx( server, STELLAR_TOKEN_MESSENGER_MINTER, "deposit_for_burn", [ new Address(stellarKeypair.publicKey()).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), mintRecipient, new Address(STELLAR_USDC).toScVal(), xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller nativeToScVal(MAX_FEE, { type: "i128" }), nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold ], ); console.log(`Burn Tx: ${txHash}`); return txHash; } ``` ### 3.3. Retrieve attestation Retrieve the attestation required to complete the CCTP transfer by calling Circle's attestation API. * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) API endpoint to retrieve the attestation. * Pass `STELLAR_DOMAIN` for the `sourceDomain` path parameter, using the [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) for Stellar Testnet (27). * Pass `transactionHash` from the value returned by `submitSorobanTx` within the `burnUSDC` function above. ```ts TypeScript theme={null} async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } ``` ### 3.4. Mint USDC on Arc Call the [`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage) from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses) deployed on Arc Testnet to mint USDC on the destination blockchain. * Pass the signed attestation and the message bytes as parameters. * The contract verifies the attestation and mints USDC to the recipient encoded in the CCTP message. ```ts TypeScript theme={null} async function mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc Testnet..."); const hash = await arcWalletClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); await arcPublicClient.waitForTransactionReceipt({ hash }); console.log(`Mint Tx: ${hash}`); } ``` ## Step 4: Full script Create an `index.ts` file in your project directory and paste the full script below so you can run the flow from one file. ```ts index.ts expandable theme={null} import { Address, Contract, Keypair, nativeToScVal, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Contract Addresses const STELLAR_TOKEN_MESSENGER_MINTER = "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP"; const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; // Transfer Parameters const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals) const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals) // Chain-specific Parameters const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; // Authentication const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); // Set up wallet clients const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } // ============ CCTP Flow Functions ============ async function approveUSDC() { console.log("Approving USDC spend on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const latestLedger = await server.getLatestLedger(); const expirationLedger = latestLedger.sequence + 100_000; const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [ new Address(stellarKeypair.publicKey()).toScVal(), new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(expirationLedger, { type: "u32" }), ]); console.log(`Approve Tx: ${approveHash}`); } async function burnUSDC() { console.log("Burning USDC on Stellar..."); // Bytes32 Formatted Parameters const evmAddressBytes32 = pad(evmAccount.address); const mintRecipient = xdr.ScVal.scvBytes( Buffer.from(evmAddressBytes32.slice(2), "hex"), ); const server = new rpc.Server(STELLAR_RPC_URL); const txHash = await submitSorobanTx( server, STELLAR_TOKEN_MESSENGER_MINTER, "deposit_for_burn", [ new Address(stellarKeypair.publicKey()).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), mintRecipient, new Address(STELLAR_USDC).toScVal(), xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller nativeToScVal(MAX_FEE, { type: "i128" }), nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold ], ); console.log(`Burn Tx: ${txHash}`); return txHash; } async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } async function mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc Testnet..."); const hash = await arcWalletClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); await arcPublicClient.waitForTransactionReceipt({ hash }); console.log(`Mint Tx: ${hash}`); } // ============ Main Execution ============ async function main() { await approveUSDC(); const burnTx = await burnUSDC(); const attestation = await retrieveAttestation(burnTx); await mintUSDCOnArc(attestation); console.log("USDC transfer from Stellar to Arc completed!"); } main().catch(console.error); ``` ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` When the transfer finishes, the console logs a completion message and the relevant transaction hashes. Successful output looks similar to the following: ```bash Shell theme={null} Approving USDC spend on Stellar... Approve Tx: Burning USDC on Stellar... Burn Tx: Retrieving attestation... Waiting for attestation... Waiting for attestation... Attestation retrieved successfully! Minting USDC on Arc Testnet... Mint Tx: 0x... USDC transfer from Stellar to Arc completed! ``` Attestation polling can take several minutes depending on network conditions and the finality threshold you chose. The script retries every 5 seconds with no timeout, so if it appears to hang at `Waiting for attestation...`, allow at least five minutes before investigating. **Rate limit:** The attestation service rate limit is 35 requests per second. If you exceed this limit, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 (Too Many Requests) response. This quickstart demonstrates how to transfer USDC from Arc Testnet to Stellar Testnet using CCTP. You use the [viem](https://viem.sh/) library to burn USDC on Arc, and [`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) to mint and forward tokens on the Stellar CCTP Forwarder. When you finish, you will have executed a full burn-attest-mint flow. You should be comfortable using a terminal and Node.js. Familiarity with basic EVM usage and Stellar Soroban transactions helps you follow and adapt the script. Examples use Arc Testnet as the source, but you can use any [supported blockchain](/cctp/concepts/supported-chains-and-domains). Always [use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/stellar-contracts). * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is set to a user account or muxed address, USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. ## Prerequisites Before you begin this tutorial, ensure you have: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an EVM wallet with the private key available for Arc Testnet * Added the Arc Testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your wallet with Arc Testnet USDC (for gas fees and the transfer amount) from the [Circle Faucet](https://faucet.circle.com) * Prepared a Stellar Testnet wallet with the secret key (`S...`) available * Funded your Stellar wallet with testnet XLM from the [Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees on Stellar Testnet) * If needed, identified the forward recipient Stellar `strkey` (`G...`, `C...`, or `M...`). For `G...` or `M...` recipients, established a [USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) before receiving minted USDC You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund accounts and establish USDC trustlines. ## Step 1: Set up the project This step shows you how to prepare your project and environment. ### 1.1. Set up your development environment Create a new directory and install the required dependencies: ```bash Shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-arc-to-stellar cd cctp-arc-to-stellar npm init -y # Set up module type and start command npm pkg set type=module npm pkg set scripts.start="npx tsx --env-file=.env index.ts" # Install runtime dependencies npm install @stellar/stellar-sdk viem # Install dev dependencies npm install --save-dev typescript @types/node ``` ### 1.2. Initialize and configure the project This step is optional. It helps prevent missing types in your IDE or editor. Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Configure environment variables Open `.env` in your editor and add: ```text theme={null} EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY # FORWARD_RECIPIENT=G_OR_C_OR_M_STELLAR_STRKEY ``` * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc Testnet. * `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban transactions on Stellar Testnet. * `FORWARD_RECIPIENT` is required when the final recipient is a `G...` or `M...` address. When omitted, the script defaults to the public key derived from `STELLAR_SECRET_KEY`. The `npm run start` command loads variables from `.env` using Node.js native env-file support. This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. ## Step 2: Configure the script This section covers the necessary setup for the transfer script, including defining keys and addresses, and configuring the wallet clients for interacting with Arc and Stellar. ### 2.1. Define configuration constants The script predefines the contract addresses, transfer amount, and other parameters. ```ts TypeScript theme={null} import { Contract, Keypair, StrKey, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Contract Addresses const ARC_USDC = "0x3600000000000000000000000000000000000000"; const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const STELLAR_CCTP_FORWARDER = "CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ"; // Transfer Parameters const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits) const MAX_FEE = 500n; // 0.0005 USDC (500 subunits) // Chain-specific Parameters const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; ``` ### 2.2. Set up wallet clients The wallet clients configure the appropriate network settings using `viem`. In this example, the script connects to Arc Testnet. ```ts TypeScript theme={null} const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); ``` ### 2.3. Encode the CCTP Forwarder hook data When transferring to Stellar, the Arc burn encodes the forward recipient in the `hookData` field. The Stellar CCTP Forwarder contract is set as both the `mintRecipient` and `destinationCaller`. The hook data encodes the forward recipient in a specific binary format: ```text Byte layout theme={null} [32-byte header + forward recipient bytes] ├─ Bytes 0-23: Zero padding (0x000...000) ├─ Bytes 24-27: Hook version (uint32, currently 0) ├─ Bytes 28-31: Forward recipient strkey length (uint32, byte length) └─ Bytes 32+: Forward recipient strkey as UTF-8 bytes ``` Example: For forward recipient "GABC...XYZ" (56 chars): * Zero padding: 0x000000000000000000000000000000000000000000000000 * Hook version: 0x00000000 * Length: 0x00000038 (56 in hex) * Forward recipient: "GABC...XYZ" encoded as UTF-8 This encoding tells the CCTP Forwarder where to send tokens after minting. `G` and `M` strkey forward recipients need an established [USDC trustline](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines) before receiving funds. Transfers without a trustline fail. This path follows the [CCTP Forwarder](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) pattern: 1. **Burn with hook**: `depositForBurnWithHook` on the Arc [`TokenMessengerV2`](/cctp/references/contract-interfaces#depositforburnwithhook) contract. `mintRecipient` is the Stellar [CCTP Forwarder](/cctp/references/stellar-contracts#cctpforwarder), and `hookData` carries the forward recipient strkey. 2. **Attest**: Circle's attestation service signs the burn event. 3. **Mint and forward**: `mint_and_forward` on the Stellar CCTP Forwarder calls `receive_message` on `MessageTransmitter`, mints tokens, and forwards them per the hook data. The CCTP Forwarder flow is non-custodial. In one atomic Soroban transaction, `mint_and_forward` mints to `CctpForwarder` and pays `forwardRecipient` onchain. Circle does not take custody of the minted balance in between. ```ts TypeScript theme={null} // Convert a Stellar contract address (C...) to 0x-prefixed bytes32 function contractStrkeyToBytes32(strkey: string): `0x${string}` { if (!StrKey.isValidContract(strkey)) { throw new Error(`Invalid contract strkey: ${strkey}`); } return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`; } // Build hook data encoding the forward recipient function buildCctpForwarderHookData( forwardRecipientStrkey: string, ): `0x${string}` { const isValid = StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) || StrKey.isValidContract(forwardRecipientStrkey) || StrKey.isValidMed25519PublicKey(forwardRecipientStrkey); if (!isValid) { throw new Error( `Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`, ); } const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8"); const hookData = Buffer.alloc(32 + recipientBytes.length); hookData.writeUInt32BE(0, 24); // hook version = 0 hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8 return `0x${hookData.toString("hex")}`; } const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); // Falls back to the Stellar public key derived from STELLAR_SECRET_KEY // when FORWARD_RECIPIENT is unset or empty. const forwardRecipient = process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey(); const hookData = buildCctpForwarderHookData(forwardRecipient); ``` ## Step 3: Implement the transfer logic This step implements the core transfer logic: approve and burn on Arc, poll for an attestation, then mint and forward on Stellar. A successful run prints transaction hashes and a completion message in the console. ### 3.1. Approve USDC on Arc Grant approval for the [`TokenMessengerV2` contract](/cctp/references/contract-addresses) to withdraw USDC from your wallet. This allows the contract to burn USDC when you initiate the transfer. ```ts TypeScript theme={null} async function approveUSDC() { console.log("Approving USDC spend on Arc..."); const approveTx = await arcWalletClient.sendTransaction({ to: ARC_USDC as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT], }), }); await arcPublicClient.waitForTransactionReceipt({ hash: approveTx }); console.log(`Approve Tx: ${approveTx}`); } ``` ### 3.2. Burn USDC on Arc Call `depositForBurnWithHook` to burn USDC with the CCTP Forwarder hook data. You specify the following parameters: * **Burn amount**: The amount of USDC to burn (in Arc subunits, 6 decimals) * **Destination domain**: The target blockchain for minting USDC (see [supported blockchains and domains](/cctp/concepts/supported-chains-and-domains)) * **Mint recipient**: The CCTP Forwarder contract address on Stellar (encoded as `bytes32`) * **Burn token**: The contract address of the USDC token being burned on Arc * **Destination caller**: The CCTP Forwarder contract address on Stellar (restricts who can call `receive_message`) * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer * **Finality threshold**: Determines whether it's a [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) (1000 or less) or a [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times) (2000 or more) * **Hook data**: Encodes the final Stellar recipient address for the CCTP Forwarder Always [use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/stellar-contracts). * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is set to a user account or muxed address, USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. `mintRecipient` and `destinationCaller` must be the Stellar CCTP Forwarder contract address. `TokenMessengerMinter` assumes `mintRecipient` is a contract address, so this example validates `STELLAR_CCTP_FORWARDER` as a contract `strkey` and uses the final recipient only in `hookData`. ```ts TypeScript theme={null} async function burnUSDC() { console.log("Burning USDC on Arc (with hook)..."); const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER); const burnTx = await arcWalletClient.sendTransaction({ to: ARC_TOKEN_MESSENGER as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ AMOUNT, STELLAR_DOMAIN, cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder ARC_USDC as `0x${string}`, cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder MAX_FEE, 2000, // Standard Transfer finality threshold hookData, ], }), }); await arcPublicClient.waitForTransactionReceipt({ hash: burnTx }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } ``` ### 3.3. Retrieve attestation Retrieve the attestation required to complete the CCTP transfer by calling Circle's attestation API. * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) API endpoint to retrieve the attestation. * Pass `ARC_TESTNET_DOMAIN` for the `sourceDomain` path parameter, using the [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) for Arc Testnet (26). * Pass `transactionHash` from the value returned by `burnUSDC` above. ```ts TypeScript theme={null} async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } ``` ### 3.4. Mint and forward USDC on Stellar The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban contract transaction. ```ts TypeScript theme={null} async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } ``` Use the `submitSorobanTx` helper to call `mint_and_forward` on the Stellar CCTP Forwarder. This verifies the CCTP message and attestation, mints USDC through the `TokenMessengerMinter`, and forwards it to the recipient encoded in the hook data: ```ts TypeScript theme={null} async function mintAndForwardOnStellar(attestation: AttestationMessage) { console.log("Minting and forwarding USDC on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const messageBytes = Buffer.from( attestation.message.replace("0x", ""), "hex", ); const attestationBytes = Buffer.from( attestation.attestation.replace("0x", ""), "hex", ); const txHash = await submitSorobanTx( server, STELLAR_CCTP_FORWARDER, "mint_and_forward", [xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)], ); console.log(`mint_and_forward Tx: ${txHash}`); } ``` ## Step 4: Full script Create an `index.ts` file in your project directory and paste the full script below so you can run the flow from one file. ```ts index.ts expandable theme={null} import { Contract, Keypair, StrKey, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Contract Addresses const ARC_USDC = "0x3600000000000000000000000000000000000000"; const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const STELLAR_CCTP_FORWARDER = "CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ"; // Transfer Parameters const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits) const MAX_FEE = 500n; // 0.0005 USDC (500 subunits) // Chain-specific Parameters const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; // Set up wallet clients const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); // Hook Data — encodes the forward recipient for the CCTP Forwarder contract function contractStrkeyToBytes32(strkey: string): `0x${string}` { if (!StrKey.isValidContract(strkey)) { throw new Error(`Invalid contract strkey: ${strkey}`); } return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`; } function buildCctpForwarderHookData( forwardRecipientStrkey: string, ): `0x${string}` { const isValid = StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) || StrKey.isValidContract(forwardRecipientStrkey) || StrKey.isValidMed25519PublicKey(forwardRecipientStrkey); if (!isValid) { throw new Error( `Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`, ); } const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8"); const hookData = Buffer.alloc(32 + recipientBytes.length); hookData.writeUInt32BE(0, 24); hookData.writeUInt32BE(recipientBytes.length, 28); recipientBytes.copy(hookData, 32); return `0x${hookData.toString("hex")}`; } const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); // Falls back to the Stellar public key derived from STELLAR_SECRET_KEY // when FORWARD_RECIPIENT is unset or empty. const forwardRecipient = process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey(); const hookData = buildCctpForwarderHookData(forwardRecipient); // ============ CCTP Flow Functions ============ async function approveUSDC() { console.log("Approving USDC spend on Arc..."); const approveTx = await arcWalletClient.sendTransaction({ to: ARC_USDC as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], }, ], functionName: "approve", args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT], }), }); await arcPublicClient.waitForTransactionReceipt({ hash: approveTx }); console.log(`Approve Tx: ${approveTx}`); } async function burnUSDC() { console.log("Burning USDC on Arc (with hook)..."); const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER); const burnTx = await arcWalletClient.sendTransaction({ to: ARC_TOKEN_MESSENGER as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurnWithHook", stateMutability: "nonpayable", inputs: [ { name: "amount", type: "uint256" }, { name: "destinationDomain", type: "uint32" }, { name: "mintRecipient", type: "bytes32" }, { name: "burnToken", type: "address" }, { name: "destinationCaller", type: "bytes32" }, { name: "maxFee", type: "uint256" }, { name: "minFinalityThreshold", type: "uint32" }, { name: "hookData", type: "bytes" }, ], outputs: [], }, ], functionName: "depositForBurnWithHook", args: [ AMOUNT, STELLAR_DOMAIN, cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder ARC_USDC as `0x${string}`, cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder MAX_FEE, 2000, // Standard Transfer finality threshold hookData, ], }), }); await arcPublicClient.waitForTransactionReceipt({ hash: burnTx }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } async function mintAndForwardOnStellar(attestation: AttestationMessage) { console.log("Minting and forwarding USDC on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const messageBytes = Buffer.from( attestation.message.replace("0x", ""), "hex", ); const attestationBytes = Buffer.from( attestation.attestation.replace("0x", ""), "hex", ); const txHash = await submitSorobanTx( server, STELLAR_CCTP_FORWARDER, "mint_and_forward", [xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)], ); console.log(`mint_and_forward Tx: ${txHash}`); } // ============ Main Execution ============ async function main() { await approveUSDC(); const burnTx = await burnUSDC(); const attestation = await retrieveAttestation(burnTx); await mintAndForwardOnStellar(attestation); console.log("USDC transfer from Arc to Stellar completed!"); } main().catch(console.error); ``` ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` When the transfer finishes, the console logs a completion message and the relevant transaction hashes. Successful output looks similar to the following: ```bash Shell theme={null} Approving USDC spend on Arc... Approve Tx: 0x65d6504333ac76cf952975dad29d4a31d8de28c59d7c97ee8ac5d0c360c0e70c Burning USDC on Arc (with hook)... Burn Tx: 0x73e1eb9224ca5778be763b4e8afc11cfa63e7ae3caa8b2748ce73f4f3d07181a Retrieving attestation... Waiting for attestation... Attestation retrieved successfully! Minting and forwarding USDC on Stellar... mint_and_forward Tx: USDC transfer from Arc to Stellar completed! ``` Attestation polling can take several minutes depending on network conditions and the finality threshold you chose. The script retries every 5 seconds with no timeout, so if it appears to hang at `Waiting for attestation...`, allow at least five minutes before investigating. **Rate limit:** The attestation service rate limit is 35 requests per second. If you exceed this limit, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 (Too Many Requests) response. # Attestation Verification Source: https://developers.circle.com/cctp/references/attestation-verification Technical reference for verifying CCTP attestation signatures When you retrieve an attestation from Circle's Attestation Service, you can optionally verify the attestation signature before using it to mint USDC on the destination blockchain. This page explains how the verification process works and when you might want to use it. ## How verification works The verification process uses cryptographic signature recovery to confirm that Circle's Attestation Service signed the message. It involves the following steps: Fetch Circle's current public key from the [`GET /v2/publicKeys`](/api-reference/cctp/all/get-public-keys-v2) endpoint. Create a `keccak256` hash of the message bytes. Split the 65-byte attestation into its `r`, `s`, and `v` components (ECDSA signature format). Use the signature and message hash to recover the public key that signed the message. Convert both the recovered public key and Circle's public key to Ethereum addresses and compare them. If the addresses match, the attestation was signed by Circle's Attestation Service and is valid. ## When to verify attestations Attestation verification is optional because the CCTP contracts on the destination blockchain perform their own verification when you call `receiveMessage`. However, you might want to verify attestations before submitting the mint transaction if: * **Your application requires an additional layer of security**: Verifying before minting provides defense-in-depth by catching invalid attestations at the application layer. * **You want to detect invalid attestations before paying gas fees**: If an attestation is invalid, the mint transaction fails and you lose the gas fees. Pre-verification lets you catch this before submitting the transaction. * **You're building a relayer service that batches multiple attestations**: Relayers can verify each attestation in a batch before submitting, preventing a single invalid attestation from affecting the entire batch. ## Verification code example The following examples show how to verify an attestation signature using Viem or Ethers: ```ts Viem theme={null} import { keccak256, hexToBytes, recoverAddress, bytesToHex } from "viem"; interface PublicKey { publicKey: `0x${string}`; cctpVersion: number; } interface AttestationData { message: string; attestation: string; } function publicKeyToAddress(publicKey: `0x${string}`): `0x${string}` { // Remove '0x04' prefix (uncompressed public key marker) const publicKeyWithoutPrefix = `0x${publicKey.slice(4)}` as `0x${string}`; const hash = keccak256(hexToBytes(publicKeyWithoutPrefix)); // Take last 20 bytes (40 hex chars) as address return `0x${hash.slice(-40)}`; } async function getPublicKeys() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/publicKeys", ); const data = await response.json(); return data.publicKeys .filter((key: PublicKey) => key.cctpVersion === 2) .map((key: PublicKey) => key.publicKey); } async function verifyAttestation( attestationData: AttestationData, publicKeys: `0x${string}`[], ) { try { const messageHash = keccak256(attestationData.message as `0x${string}`); const attestationBytes = hexToBytes( attestationData.attestation as `0x${string}`, ); const signatureLength = 65; const numSignatures = attestationBytes.length / signatureLength; if (attestationBytes.length % signatureLength !== 0) { throw new Error(`Invalid attestation length: ${attestationBytes.length}`); } let validSignatures = 0; for (let i = 0; i < numSignatures; i++) { const start = i * signatureLength; const signature = attestationBytes.slice(start, start + signatureLength); const recoveredAddress = await recoverAddress({ hash: messageHash, signature: bytesToHex(signature), }); const isValid = publicKeys.some( (publicKey) => publicKeyToAddress(publicKey).toLowerCase() === recoveredAddress.toLowerCase(), ); if (isValid) validSignatures++; } const threshold = Math.ceil(publicKeys.length / 2); console.log( `Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`, ); return validSignatures >= threshold; } catch (error) { console.error( "Error verifying attestation:", error instanceof Error ? error.message : String(error), ); return false; } } const attestationData: AttestationData = { message: "0x000000010000001a00000015...", // Full message hex from API attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API }; // Example usage const publicKeys = await getPublicKeys(); const isValid = await verifyAttestation(attestationData, publicKeys); ``` ```ts Ethers.js theme={null} import { ethers } from "ethers"; interface PublicKey { publicKey: string; cctpVersion: number; } interface AttestationData { message: string; attestation: string; } async function getPublicKeys() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/publicKeys", ); const data = await response.json(); // Get all public keys for CCTP V2 const v2Keys = data.publicKeys .filter((key: PublicKey) => key.cctpVersion === 2) .map((key: PublicKey) => key.publicKey); if (v2Keys.length === 0) { throw new Error("CCTP V2 public key not found"); } return v2Keys; } function verifyAttestation( attestationData: AttestationData, publicKeys: string[], ) { try { const messageHash = ethers.keccak256(attestationData.message); const attestationBytes = ethers.getBytes(attestationData.attestation); // V2 attestation has multiple 65-byte signatures const signatureLength = 65; const numSignatures = attestationBytes.length / signatureLength; if (attestationBytes.length % signatureLength !== 0) { throw new Error(`Invalid attestation length: ${attestationBytes.length}`); } let validSignatures = 0; // Verify each signature for (let i = 0; i < numSignatures; i++) { const start = i * signatureLength; const sigBytes = attestationBytes.slice(start, start + signatureLength); const r = ethers.hexlify(sigBytes.slice(0, 32)); const s = ethers.hexlify(sigBytes.slice(32, 64)); const v = sigBytes[64]; const signature = { r, s, v }; const recoveredAddress = ethers.recoverAddress(messageHash, signature); // Check if recovered address matches any V2 public key const isValid = publicKeys.some( (publicKey) => ethers.computeAddress(publicKey).toLowerCase() === recoveredAddress.toLowerCase(), ); if (isValid) validSignatures++; } const threshold = Math.ceil(publicKeys.length / 2); console.log( `Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`, ); return validSignatures >= threshold; } catch (error) { console.error("Error verifying attestation:", (error as Error).message); return false; } } // Use attestation data from the API const attestationData: AttestationData = { message: "0x000000010000001a00000015...", // Full message hex from API attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API }; // Example usage const publicKeys = await getPublicKeys(); const isValid = verifyAttestation(attestationData, publicKeys); ``` # Contract Addresses Source: https://developers.circle.com/cctp/references/contract-addresses CCTP smart contract addresses for EVM-compatible blockchains This page lists the deployed contract addresses for CCTP on all [supported EVM-compatible blockchains](/cctp/concepts/supported-chains-and-domains). For contract interfaces and method signatures, see [Contract Interfaces](/cctp/references/contract-interfaces). Full contract source code is [available on GitHub](https://github.com/circlefin/evm-cctp-contracts). For non-EVM blockchain contract addresses, see: * [Solana Programs](/cctp/references/solana-programs) * [Stellar Contracts](/cctp/references/stellar-contracts) * [Starknet Contracts](/cctp/references/starknet-contracts) ## Mainnet contract addresses ### TokenMessengerV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Avalanche** | 1 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://snowtrace.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **OP Mainnet** | 2 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://optimistic.etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Arbitrum** | 3 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://arbiscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Base** | 6 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://basescan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Polygon PoS** | 7 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://polygonscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Unichain** | 10 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://uniscan.xyz/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Linea** | 11 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://lineascan.build/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Codex** | 12 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.codex.xyz/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Sonic** | 13 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://sonicscan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **World Chain** | 14 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://worldscan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Monad** | 15 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://monadvision.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Sei** | 16 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://seiscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **XDC** | 18 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://xdcscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **HyperEVM** | 19 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://hyperscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Ink** | 21 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.inkonchain.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Plume** | 22 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.plume.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **EDGE** | 28 | [`0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c`](https://pro.edgex.exchange/en-US/explorer/address/0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c) | | **Injective** | 29 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://blockscout.injective.network/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Morph** | 30 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.morph.network/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **Pharos** | 31 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://pharos.socialscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | ### MessageTransmitterV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://etherscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Avalanche** | 1 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://snowtrace.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **OP Mainnet** | 2 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://optimistic.etherscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Arbitrum** | 3 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://arbiscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Base** | 6 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://basescan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Polygon PoS** | 7 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://polygonscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Unichain** | 10 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://uniscan.xyz/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Linea** | 11 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://lineascan.build/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Codex** | 12 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.codex.xyz/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Sonic** | 13 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://sonicscan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **World Chain** | 14 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://worldscan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Monad** | 15 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://monadvision.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Sei** | 16 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://seiscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **XDC** | 18 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://xdcscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **HyperEVM** | 19 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://hyperscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Ink** | 21 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.inkonchain.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Plume** | 22 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.plume.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **EDGE** | 28 | [`0x5b61381Fc9e58E70EfC13a4A97516997019198ee`](https://pro.edgex.exchange/en-US/explorer/address/0x5b61381Fc9e58E70EfC13a4A97516997019198ee) | | **Injective** | 29 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://blockscout.injective.network/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Morph** | 30 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.morph.network/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **Pharos** | 31 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://pharos.socialscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | ### TokenMinterV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://etherscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Avalanche** | 1 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://snowtrace.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **OP Mainnet** | 2 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://optimistic.etherscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Arbitrum** | 3 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://arbiscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Base** | 6 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://basescan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Polygon PoS** | 7 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://polygonscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Unichain** | 10 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://uniscan.xyz/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Linea** | 11 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://lineascan.build/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Codex** | 12 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.codex.xyz/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Sonic** | 13 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://sonicscan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **World Chain** | 14 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://worldscan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Monad** | 15 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://monadvision.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Sei** | 16 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://seiscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **XDC** | 18 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://xdcscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **HyperEVM** | 19 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://hyperscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Ink** | 21 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.inkonchain.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Plume** | 22 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.plume.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **EDGE** | 28 | [`0x338Dfd607855BeEc17f33e539Ac2479853cC8384`](https://pro.edgex.exchange/en-US/explorer/address/0x338Dfd607855BeEc17f33e539Ac2479853cC8384) | | **Injective** | 29 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://blockscout.injective.network/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Morph** | 30 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.morph.network/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **Pharos** | 31 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://pharos.socialscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) | ### MessageV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://etherscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Avalanche** | 1 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://snowtrace.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **OP Mainnet** | 2 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://optimistic.etherscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Arbitrum** | 3 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://arbiscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Base** | 6 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://basescan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Polygon PoS** | 7 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://polygonscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Unichain** | 10 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://uniscan.xyz/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Linea** | 11 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://lineascan.build/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Codex** | 12 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.codex.xyz/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Sonic** | 13 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://sonicscan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **World Chain** | 14 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://worldscan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Monad** | 15 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://monadvision.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Sei** | 16 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://seiscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **XDC** | 18 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://xdcscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **HyperEVM** | 19 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://hyperscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Ink** | 21 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.inkonchain.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Plume** | 22 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.plume.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **EDGE** | 28 | [`0x88ba38dbB2117879E500c11A0772e2B84Be000B3`](https://pro.edgex.exchange/en-US/explorer/address/0x88ba38dbB2117879E500c11A0772e2B84Be000B3) | | **Injective** | 29 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://blockscout.injective.network/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Morph** | 30 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.morph.network/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **Pharos** | 31 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://pharos.socialscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | ## Testnet contract addresses ### TokenMessengerV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum Sepolia** | 0 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.etherscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Avalanche Fuji** | 1 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.snowtrace.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **OP Sepolia** | 2 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia-optimism.etherscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Arbitrum Sepolia** | 3 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.arbiscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Base Sepolia** | 6 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://base-sepolia.blockscout.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Polygon PoS Amoy** | 7 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://amoy.polygonscan.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Unichain Sepolia** | 10 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://unichain-sepolia.blockscout.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Linea Sepolia** | 11 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.lineascan.build/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Codex Testnet** | 12 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer.codex-stg.xyz/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Sonic Testnet** | 13 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://blaze.soniclabs.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **World Chain Sepolia** | 14 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.worldscan.org/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Monad Testnet** | 15 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.monadexplorer.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Sei Testnet** | 16 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.seiscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **XDC Apothem** | 18 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.xdcscan.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **HyperEVM Testnet** | 19 | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | | **Ink Testnet** | 21 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer-sepolia.inkonchain.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Plume Testnet** | 22 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet-explorer.plume.org/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Arc Testnet** | 26 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **EDGE Testnet** | 28 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://edge-testnet.explorer.alchemy.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Injective Testnet** | 29 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.blockscout.injective.network/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Morph Hoodi Testnet** | 30 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer-hoodi.morph.network/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **Pharos Testnet** | 31 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://pharos-testnet.socialscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | ### MessageTransmitterV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum Sepolia** | 0 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.etherscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Avalanche Fuji** | 1 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.snowtrace.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **OP Sepolia** | 2 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia-optimism.etherscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Arbitrum Sepolia** | 3 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.arbiscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Base Sepolia** | 6 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://base-sepolia.blockscout.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Polygon PoS Amoy** | 7 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://amoy.polygonscan.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Unichain Sepolia** | 10 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://unichain-sepolia.blockscout.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Linea Sepolia** | 11 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.lineascan.build/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Codex Testnet** | 12 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer.codex-stg.xyz/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Sonic Testnet** | 13 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://blaze.soniclabs.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **World Chain Sepolia** | 14 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.worldscan.org/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Monad Testnet** | 15 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.monadexplorer.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Sei Testnet** | 16 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.seiscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **XDC Apothem** | 18 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.xdcscan.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **HyperEVM Testnet** | 19 | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` | | **Ink Testnet** | 21 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer-sepolia.inkonchain.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Plume Testnet** | 22 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet-explorer.plume.org/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Arc Testnet** | 26 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.arcscan.app/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **EDGE Testnet** | 28 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://edge-testnet.explorer.alchemy.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Injective Testnet** | 29 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.blockscout.injective.network/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Morph Hoodi Testnet** | 30 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer-hoodi.morph.network/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **Pharos Testnet** | 31 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://pharos-testnet.socialscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | ### TokenMinterV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum Sepolia** | 0 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.etherscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Avalanche Fuji** | 1 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.snowtrace.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **OP Sepolia** | 2 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia-optimism.etherscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Arbitrum Sepolia** | 3 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.arbiscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Base Sepolia** | 6 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://base-sepolia.blockscout.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Polygon PoS Amoy** | 7 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://amoy.polygonscan.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Unichain Sepolia** | 10 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://unichain-sepolia.blockscout.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Linea Sepolia** | 11 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.lineascan.build/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Codex Testnet** | 12 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer.codex-stg.xyz/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Sonic Testnet** | 13 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://blaze.soniclabs.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **World Chain Sepolia** | 14 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.worldscan.org/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Monad Testnet** | 15 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.monadexplorer.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Sei Testnet** | 16 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.seiscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **XDC Apothem** | 18 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.xdcscan.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **HyperEVM Testnet** | 19 | `0xb43db544E2c27092c107639Ad201b3dEfAbcF192` | | **Ink Testnet** | 21 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer-sepolia.inkonchain.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Plume Testnet** | 22 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet-explorer.plume.org/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Arc Testnet** | 26 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.arcscan.app/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **EDGE Testnet** | 28 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://edge-testnet.explorer.alchemy.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Injective Testnet** | 29 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.blockscout.injective.network/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Morph Hoodi Testnet** | 30 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer-hoodi.morph.network/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **Pharos Testnet** | 31 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://pharos-testnet.socialscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | ### MessageV2 | Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum Sepolia** | 0 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.etherscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Avalanche Fuji** | 1 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.snowtrace.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **OP Sepolia** | 2 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia-optimism.etherscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Arbitrum Sepolia** | 3 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.arbiscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Base Sepolia** | 6 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://base-sepolia.blockscout.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Polygon PoS Amoy** | 7 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://amoy.polygonscan.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Unichain Sepolia** | 10 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://unichain-sepolia.blockscout.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Linea Sepolia** | 11 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.lineascan.build/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Codex Testnet** | 12 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer.codex-stg.xyz/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Sonic Testnet** | 13 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://blaze.soniclabs.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **World Chain Sepolia** | 14 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.worldscan.org/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Monad Testnet** | 15 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.monadexplorer.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Sei Testnet** | 16 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.seiscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **XDC Apothem** | 18 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.xdcscan.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **HyperEVM Testnet** | 19 | `0xbaC0179bB358A8936169a63408C8481D582390C4` | | **Ink Testnet** | 21 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer-sepolia.inkonchain.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Plume Testnet** | 22 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet-explorer.plume.org/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Arc Testnet** | 26 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.arcscan.app/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **EDGE Testnet** | 28 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://edge-testnet.explorer.alchemy.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Injective Testnet** | 29 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.blockscout.injective.network/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Morph Hoodi Testnet** | 30 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer-hoodi.morph.network/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **Pharos Testnet** | 31 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://pharos-testnet.socialscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | # EVM Contract Interfaces Source: https://developers.circle.com/cctp/references/contract-interfaces Public methods and events for CCTP smart contracts on EVM-compatible blockchains This page documents the public methods and events exposed by CCTP smart contracts on EVM-compatible blockchains. ## Contract responsibilities * **TokenMessengerV2**: Entrypoint for crosschain USDC transfer. Routes messages to burn USDC on a source blockchain and mint USDC on a destination blockchain. * **MessageTransmitterV2**: Generic message passing. Sends all messages on the source blockchain and receives all messages on the destination blockchain. * **TokenMinterV2**: Responsible for minting and burning USDC. Contains blockchain-specific settings used by burners and minters. * **MessageV2**: Provides helper functions for crosschain transfers, such as `bytes32ToAddress` and `addressToBytes32`, which are commonly used when bridging between EVM and non-EVM blockchains. **Gas optimization tip:** If you're writing your own integration, it's more gas-efficient to [include address conversion logic directly in your contract](https://github.com/circlefin/evm-cctp-contracts/blob/5f1901a9791b18204e8556bb53fb0dfcb05a832a/src/messages/Message.sol#L146) rather than calling an external contract. Full contract source code is [available on GitHub](https://github.com/circlefin/evm-cctp-contracts). ## TokenMessengerV2 ### depositForBurn Deposits and burns tokens from sender to be minted on destination domain. Minted tokens will be transferred to `mintRecipient`. **Note:** There is a \$10 million limit on the amount of USDC that can be burned in a single CCTP transaction. If the amount exceeds this limit, the transaction will revert. If you need to transfer more than this limit, break up your transfers into multiple transactions. For Fast Transfers, you should always [check the remaining allowance](/api-reference/cctp/all/get-fast-burn-usdc-allowance) before initiating a transfer to ensure there is enough to complete your transfer. **Parameters** | Field | Type | Description | | ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `amount` | `uint256` | Amount of tokens to deposit and burn | | `destinationDomain` | `uint32` | Destination [domain ID](/cctp/concepts/supported-chains-and-domains#domain-identifiers) to send the message to | | `mintRecipient` | `bytes32` | Address of mint recipient on destination domain (must be converted to 32 byte array, that is, prefix with zeros if needed) | | `burnToken` | `address` | Address of contract to burn deposited tokens on local domain | | `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` | | `maxFee` | `uint256` | Maximum [fee](/cctp/concepts/fees) paid for transfer, specified in units of `burnToken` | | `minFinalityThreshold` | `uint32` | Minimum [finality threshold](/cctp/concepts/finality-and-block-confirmations) at which burn will be attested | **Example** ```solidity Solidity theme={null} // Burn 100 USDC on Ethereum for minting on Avalanche uint256 amount = 100 * 10**6; // 100 USDC uint32 destinationDomain = 1; // Avalanche bytes32 mintRecipient = bytes32(uint256(uint160(recipientAddress))); address burnToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC on Ethereum bytes32 destinationCaller = bytes32(0); // Anyone can call receiveMessage uint256 maxFee = 1000; // 0.001 USDC max fee uint32 minFinalityThreshold = 1000; // Fast Transfer tokenMessenger.depositForBurn( amount, destinationDomain, mintRecipient, burnToken, destinationCaller, maxFee, minFinalityThreshold ); ``` ### depositForBurnWithHook Deposits and burns tokens from sender to be minted on destination domain, and emits a crosschain message with additional hook data appended. In addition to the standard `depositForBurn` parameters, `depositForBurnWithHook` accepts a dynamic-length `hookData` parameter, allowing you to include additional metadata that can trigger custom logic on the destination blockchain. **Note:** There is a \$10 million limit on the amount of USDC that can be burned in a single CCTP transaction. If the amount exceeds this limit, the transaction will revert. If you need to transfer more than this limit, break up your transfers into multiple transactions. For Fast Transfers, you should always [check the remaining allowance](/api-reference/cctp/all/get-fast-burn-usdc-allowance) before initiating a transfer to ensure there is enough to complete your transfer. **Parameters** | Field | Type | Description | | ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `amount` | `uint256` | Amount of tokens to burn | | `destinationDomain` | `uint32` | Destination domain to send the message to | | `mintRecipient` | `bytes32` | Address of mint recipient on destination domain (must be converted to 32 byte array, that is, prefix with zeros if needed) | | `burnToken` | `address` | Address of contract to burn deposited tokens on local domain | | `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` | | `maxFee` | `uint256` | Maximum fee paid for transfer, specified in units of `burnToken` | | `minFinalityThreshold` | `uint32` | Minimum finality threshold at which burn will be attested | | `hookData` | `bytes` | Additional metadata attached to the attested message, used to trigger custom logic on the destination blockchain | ### getMinFeeAmount Calculates and returns the minimum fee required for a given amount in a Standard Transfer. If the minimum fee (per unit of `burnToken`) is non-zero, the specified `maxFee` must be at least the returned minimum fee. Otherwise, the burn will revert onchain. **Parameters** | Field | Type | Description | | -------- | --------- | ----------------------------------------------------------------------------------------------- | | `amount` | `uint256` | The amount used to compute the minimum fee. Must be greater than `1` if standard fee is applied | ### handleReceiveFinalizedMessage Handles incoming message received by the local MessageTransmitter. For a burn message, mints the associated token to the requested recipient on the local domain. Validates the function sender is the local MessageTransmitter, and the remote sender is a registered remote TokenMessenger for `remoteDomain`. This method is called for messages where `finalityThresholdExecuted` ≥ 2000 (Standard Transfer). **Parameters** | Field | Type | Description | | --------------------------- | ------------------------ | -------------------------------------------------------------- | | `remoteDomain` | `uint32` | The domain where the message originated from | | `sender` | `bytes32` | The sender of the message (remote TokenMessenger) | | `finalityThresholdExecuted` | `uint32` | Specifies the level of finality Circle signed the message with | | `messageBody` | `bytes` (dynamic length) | The message body bytes | ### handleReceiveUnfinalizedMessage Handles incoming message received by the local MessageTransmitter. For a burn message, mints the associated token to the requested recipient on the local domain. Similar to `handleReceiveFinalizedMessage`, but is called for messages which are not finalized (`finalityThresholdExecuted` \< 2000) such as Fast Transfers. Unlike `handleReceiveFinalizedMessage`, `handleReceiveUnfinalizedMessage` processes messages with: * **`expirationBlock`**: If `expirationBlock` ≤ `blockNumber` on the destination domain, the message will revert and must be re-signed without the expiration block. * **`feeExecuted`**: If nonzero, the `feeExecuted` amount is minted to the `feeRecipient`. **Parameters** | Field | Type | Description | | --------------------------- | ------------------------ | -------------------------------------------------------------------------------------------- | | `remoteDomain` | `uint32` | The domain where the message originated from | | `sender` | `bytes32` | The sender of the message (remote TokenMessenger) | | `finalityThresholdExecuted` | `uint32` | Specifies the level of finality Circle signed the message with | | `messageBody` | `bytes` (dynamic length) | The message body bytes (see [Message format](/cctp/references/technical-guide#message-body)) | ## MessageTransmitterV2 ### `receiveMessage` Receives message on destination blockchain by passing message and attestation. Emits `MessageReceived` event. Messages with a given nonce can only be broadcast successfully once for a pair of domains. The message body of a valid message is passed to the specified recipient for further processing. **Parameters** | Field | Type | Description | | ------------- | ------- | ------------------------------------------------------------------------------------- | | `message` | `bytes` | Encoded message (see [Message format](/cctp/references/technical-guide#message-body)) | | `attestation` | `bytes` | Signed attestation received from Circle's attestation service | **Example** ```solidity Solidity theme={null} // Mint USDC on destination chain bytes memory message = attestationData.message; bytes memory attestation = attestationData.attestation; messageTransmitter.receiveMessage(message, attestation); ``` ### `sendMessage` Sends a message to the recipient on the destination domain. Emits a `MessageSent` event which will be attested by Circle's attestation service. **Parameters** | Field | Type | Description | | ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `destinationDomain` | `uint32` | Destination domain ID to send the message to | | `recipient` | `bytes32` | Address of recipient on destination domain | | `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` | | `minFinalityThreshold` | `uint32` | Minimum finality threshold requested. A value greater than 2000 is interpreted as 2000 (finalized). Thresholds: 1000 for Fast Transfer (confirmed), 2000 for Standard Transfer (finalized) | | `messageBody` | `bytes` | application-specific message to be handled by recipient | ## Events ### DepositForBurn Emitted when USDC is burned on the source blockchain. **Parameters** | Field | Type | Indexed | Description | | --------------------------- | --------- | ------- | ----------------------------------------------------------- | | `nonce` | `uint64` | Yes | Unique message identifier | | `burnToken` | `address` | Yes | Address of token burned | | `amount` | `uint256` | No | Burn amount | | `depositor` | `address` | Yes | Address of depositor | | `mintRecipient` | `bytes32` | No | Mint recipient address on destination domain | | `destinationDomain` | `uint32` | No | Destination domain identifier | | `destinationTokenMessenger` | `bytes32` | No | Address of TokenMessenger contract on destination domain | | `destinationCaller` | `bytes32` | No | Authorized caller of `receiveMessage` on destination domain | | `maxFee` | `uint256` | No | Maximum fee for the transfer | | `minFinalityThreshold` | `uint32` | No | Minimum finality threshold at which burn will be attested | ### MessageSent Emitted when a message is sent from the source blockchain. **Parameters** | Field | Type | Indexed | Description | | --------- | ------- | ------- | -------------------- | | `message` | `bytes` | No | Raw bytes of message | ### MessageReceived Emitted when a message is received on the destination blockchain. **Parameters** | Field | Type | Indexed | Description | | -------------- | --------- | ------- | ------------------------------------------ | | `caller` | `address` | Yes | Address that called `receiveMessage` | | `sourceDomain` | `uint32` | Yes | Source domain identifier | | `nonce` | `uint64` | Yes | Unique message identifier | | `sender` | `bytes32` | No | Address of message sender on source domain | | `messageBody` | `bytes` | No | Message body | ### MintAndWithdraw Emitted when USDC is minted on the destination blockchain. **Parameters** | Field | Type | Indexed | Description | | --------------- | --------- | ------- | ----------------------------- | | `mintRecipient` | `address` | Yes | Address receiving minted USDC | | `amount` | `uint256` | No | Amount minted | | `mintToken` | `address` | Yes | Address of token minted | # CoreDepositWallet Contract Interface Source: https://developers.circle.com/cctp/references/coredepositwallet-contract-interface The `CoreDepositWallet` contract on HyperEVM allows you to deposit USDC from HyperEVM to HyperCore. This topic describes the contract interface and the available deposit functions. To move USDC from HyperEVM to HyperCore, always call one of the `CoreDepositWallet` deposit functions (`deposit`, `depositFor`, or `depositWithAuth`). Only USDC is supported. Sending USDC or any tokens directly to the `CoreDepositWallet` contract address doesn't trigger a deposit on HyperCore. The funds are permanently stuck. ## Deposit functions The `CoreDepositWallet` provides three entry points for depositing USDC from HyperEVM into HyperCore. All deposits credit a user's balance on HyperCore, on either the perps or spot DEX. ### `deposit` function The `deposit` function transfers USDC from the caller's address and credits the same address on HyperCore, after the caller has approved the `CoreDepositWallet` to spend their tokens. **Signature:** ```solidity theme={null} deposit(uint256 amount, uint32 destinationDex); ``` **Parameters:** | Parameter | Value | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `amount` | The USDC amount being deposited from HyperEVM to HyperCore | | `destinationDex` | The HyperCore destination `dex` index. Accepted values are:
- `0` → default perps DEX
- `type(uint32).max` → spot DEX | **Token pull:** Uses `transferFrom(msg.sender, address(this), amount)` → requires prior ERC-20 approve from the `msg.sender` to the core deposit wallet. **Who is credited:** The `msg.sender` of the transaction on HyperEVM is credited on HyperCore. **Examples:** ERC-20 Approval: ```shell theme={null} # approve the CoreDepositWallet to spend 100 USDC from the sender cast send "approve(address,uint256)" 100000000 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` Depositing to the perps DEX: ```shell theme={null} # Deposit 100 USDC to the perps DEX (destinationDex = 0) cast send "deposit(uint256,uint32)" 100000000 0 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` Depositing to the spot DEX: ```shell theme={null} # Deposit 100 USDC to the spot DEX (destinationDex = uint32.max) cast send "deposit(uint256,uint32)" 100000000 4294967295 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` **Note:** If the destination DEX value is not supported (spot or perps), the deposit is credited to the sender's spot balance. ### `depositFor` function The `depositFor` function transfers USDC from the caller but credits a specified recipient address on HyperCore. This allows deposits on behalf of another user. **Signature:** ```solidity theme={null} depositFor(address recipient, uint256 amount, uint32 destinationId); ``` **Parameters:** | Parameter | Value | | --------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `recipient` | The recipient address on HyperCore | | `amount` | The USDC amount being deposited from HyperEVM to HyperCore | | `destinationId` | The HyperCore destination `dex` index. Accepted values are:
- `0` → default perps DEX
- `type(uint32).max` → spot DEX | **Token pull:** Uses `transferFrom(msg.sender, address(this), amount)` → requires prior ERC-20 approve from the `msg.sender` to the core deposit wallet. **Who is credited:** The recipient address passed to the function is credited on HyperCore. **Examples:** ERC-20 Approval: ```shell theme={null} # approve the CoreDepositWallet to spend 100 USDC from the sender cast send "approve(address,uint256)" 100000000 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` Depositing to the perps DEX: ```shell theme={null} # Deposit 100 USDC to the perps DEX (destinationDex = 0) cast send "depositFor(address,uint256,uint32)" 100000000 0 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` Depositing to the spot DEX: ```shell theme={null} # Deposit 100 USDC to the spot DEX (destinationDex = uint32.max) cast send "depositFor(address,uint256,uint32)" 100000000 4294967295 \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` **Note:** If the destination DEX value is not supported (spot or perps), the deposit is credited to the recipient's spot balance. ### `depositWithAuth` function The `depositWithAuth` function allows depositing USDC using a pre-signed ERC-3009 authorization. This enables a deposit where the token transfer is authorized offchain and executed onchain without requiring a prior approve call. **Signature:** ```solidity theme={null} depositWithAuth(uint256 amount, uint256 authValidAfter, uint256 authValidBefore, bytes32 authNonce, uint8 v, bytes32 r, bytes32 s, uint32 destinationDex); ``` **Parameters:** * `amount`: The USDC amount being deposited from HyperEVM to HyperCore * `authValidAfter`, `authValidBefore`, `authNonce`, `v`, `r`, `s`: EIP-3009-style authorization fields for `receiveWithAuthorization` * `destinationDex`: The HyperCore destination `dex` index. Accepted values are: * `0` → default perps DEX * `type(uint32).max` → spot DEX **Token pull:** Calls `token.receiveWithAuthorization(...)`, no prior approve needed. **Who is credited:** The `msg.sender` which has to match the `from` address from the `receiveWithAuthorization` is credited on HyperCore. **receiveWithAuthorization details:** * **ERC:** ERC-3009 * **Function Signature:** `ReceiveWithAuthorization` * **Parameters:** | Parameter | Value | | ------------- | -------------------------------------------------------------------------------------------------- | | `from` | The payer's address (`authorizer`) has to match the `msg.sender` of the `depositWithAuth` function | | `to` | The `CoreDepositWallet` address (payee) | | `value` | The auth amount | | `validAfter` | The time after which this is valid (Unix time) | | `validBefore` | The time before which this is valid (Unix time) | | `nonce` | Unique nonce | | `v` | v of the signature | | `r` | r of the signature | | `s` | s of the signature | **Example:** The example below illustrates how to generate an ERC-3009 authorization: ```javascript theme={null} #!/usr/bin/env node const ethers = require("ethers"); const PRIVATE_KEY = process.env.PRIVATE_KEY; const wallet = new ethers.Wallet(PRIVATE_KEY); const provider = new ethers.JsonRpcProvider( process.env.RPC_URL || "https://rpc.hyperliquid-testnet.xyz/evm", ); const usdcAddress = ""; const coreDepositWallet = ""; const EIP712_PREFIX = "0x1901"; const amount = ethers.parseUnits("100", 6); // 100 USDC const nonce = ethers.hexlify(ethers.randomBytes(32)); const validAfter = 0; const validBefore = Math.floor(Date.now() / 1000) + 3600; // valid for 1 hour // Minimal ABI for DOMAIN_SEPARATOR const USDC_ABI = [ { inputs: [], name: "DOMAIN_SEPARATOR", outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], stateMutability: "view", type: "function", }, ]; async function getDomainSeparator(usdc) { try { return await usdc.DOMAIN_SEPARATOR(); } catch { const domain = { name: "USD Coin", version: "2", chainId: await provider.getNetwork().then((n) => n.chainId), verifyingContract: await usdc.getAddress(), }; return ethers.TypedDataEncoder.hashDomain(domain); } } async function main() { const usdc = new ethers.Contract(usdcAddress, USDC_ABI, provider); const domainSeparator = await getDomainSeparator(usdc); const structHash = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode( [ "bytes32", "address", "address", "uint256", "uint256", "uint256", "bytes32", ], [ ethers.keccak256( ethers.toUtf8Bytes( "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)", ), ), wallet.address, coreDepositWallet, amount, validAfter, validBefore, nonce, ], ), ); const digest = ethers.keccak256( ethers.concat([EIP712_PREFIX, domainSeparator, structHash]), ); const signer = new ethers.SigningKey(PRIVATE_KEY); const sig = signer.sign(digest); console.log("Authorization parameters:"); console.log({ amount: amount.toString(), validAfter, validBefore, nonce, v: sig.v, r: sig.r, s: sig.s, }); } main().catch(console.error); ``` The example below illustrates how to call the `depositWithAuth` function with the authorization data: ```shell theme={null} cast send \ "depositWithAuth(uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32,uint32)" \ 0 1735660000 0x 0x 0x \ --private-key $PRIVATE_KEY \ --rpc-url $RPC_URL ``` **Note:** If the destination DEX value is not supported (spot or `perp`), the deposit is credited to the `authorizer's` spot balance. # HyperCore CCTP-Enablement Contract Addresses Source: https://developers.circle.com/cctp/references/hypercore-contract-addresses CCTP has additional contracts beyond the standard protocol to enable transfers to HyperCore. The following sections describe the functions and addresses of these contracts. * **`CctpExtension`**: (Arbitrum only) Responsible for transferring USDC from Arbitrum to HyperCore. * **`CctpForwarder`**: (HyperEVM only) Responsible for forwarding USDC from HyperEVM to HyperCore in a CCTP transfer from any non-HyperEVM domain. * **`CoreDepositWallet`**: (HyperEVM only) Responsible for [depositing USDC into HyperCore](/cctp/references/coredepositwallet-contract-interface). This page contains the contract addresses for the HyperCore CCTP-enablement contracts. For the contract addresses for core CCTP contracts, see [EVM Contracts and Interfaces](/cctp/evm-smart-contracts), [Solana Contracts and Interfaces](/cctp/solana-programs), and [Starknet Contracts and Interfaces](/cctp/starknet-contracts). ## Mainnet contract addresses ### CctpExtension: Mainnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | ------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Arbitrum** | 3 | [`0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE`](https://arbiscan.io/address/0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE) | ### CctpForwarder: Mainnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | ------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **HyperEVM** | 19 | [`0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757`](https://hyperevmscan.io/address/0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757) | ### CoreDepositWallet: Mainnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | ------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **HyperEVM** | 19 | [`0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24`](https://hyperevmscan.io/address/0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24) | ## Testnet contract addresses ### CctpExtension: Testnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | -------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Arbitrum Sepolia** | 3 | [`0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D`](https://sepolia.arbiscan.io/address/0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D) | ### CctpForwarder: Testnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | -------------------- | ----------------------------------------------------------------- | -------------------------------------------- | | **HyperEVM Testnet** | 19 | `0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2` | ### CoreDepositWallet: Testnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | -------------------- | ----------------------------------------------------------------- | -------------------------------------------- | | **HyperEVM Testnet** | 19 | `0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206` | # CCTP Solana Programs and Interfaces Source: https://developers.circle.com/cctp/references/solana-programs Programs for CCTP support on the Solana blockchain ## Overview Solana CCTP programs are written in Rust and leverage the Anchor framework. The Solana CCTP protocol implementation is split into two programs: `MessageTransmitterV2` and `TokenMessengerMinterV2`. `TokenMessengerMinterV2` encapsulates the capabilities of both `TokenMessengerV2` and `TokenMinterV2` contracts on EVM chains. To ensure alignment with EVM contracts' logic and state, and to facilitate upgrades and maintenance, the code and state of Solana programs reflect the EVM counterparts as closely as possible. ### Mainnet program addresses | Program | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | :----------------------- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `MessageTransmitterV2` | 5 | [`CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC`](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC) | | `TokenMessengerMinterV2` | 5 | [`CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe`](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe) | ### Devnet program addresses | Program | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | :----------------------- | :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | | `MessageTransmitterV2` | 5 | [`CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC`](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC?cluster=devnet) | | `TokenMessengerMinterV2` | 5 | [`CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe`](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe?cluster=devnet) | The Solana CCTP source code is [available on GitHub](https://github.com/circlefin/solana-cctp-contracts/). The interface below serves as a reference for permissionless messaging functions exposed by the programs. ## CCTP interface The interface below serves as a reference for permissionless messaging functions exposed by the `TokenMessengerMinter` and `MessageTransmitter` programs. The full IDLs can be found onchain using a block explorer. [`MessageTransmitterV2` IDL](https://explorer.solana.com/address/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe/anchor-program) and [`TokenMessengerMinterV2` IDL](https://explorer.solana.com/address/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC/anchor-program). *See the instruction rust files or quick-start for PDA information.* ### TokenMessengerMinterV2 #### [`depositForBurn`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/deposit_for_burn.rs) Deposits and burns tokens from sender to be minted on destination domain. Minted tokens will be transferred to `mintRecipient`. **Parameters** | Field | Type | Description | | :--------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `amount` | `u64` | Amount of tokens to deposit and burn. | | `destinationDomain` | `u32` | Destination domain identifier. | | `mintRecipient` | `Pubkey` | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* | | `destinationCaller` | `Pubkey` | Address which can call `receiveMessage` on destination domain. If set to `PublicKey.default`, any address can call `receiveMessage` *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* | | `maxFee` | `u64` | Max fee paid for the transfer, specified in units of the burn token. | | `minFinalityThreshold` | `u32` | Minimum finality threshold at which burn will be attested | **Fees** A fee may be charged for standard USDC transfers. Fees for standard transfers are set to 0, but are subject to change. See [CCTP Fees](/cctp/technical-guide#cctp-fees) for more information. **MessageSent event storage** To ensure persistent and reliable message storage, MessageSent events are stored in accounts. MessageSent event accounts are generated client-side, passed into the instruction call, and assigned to have the `MessageTransmitterV2` program as the owner. See the [Quickstart Guide](/cctp/transfer-usdc-on-testnet-from-ethereum-to-avalanche) for how to generate this account and pass it to the instruction call. Message nonces are generated offchain, meaning the source messages cannot be identified from the attestation. Due to this, there is a 5 day window after sending a message that callers must wait before `reclaim_event_account` can be called. This is to ensure that the message has been fully processed by Circle's offchain services. #### [depositForBurnWithHook](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/deposit_for_burn_with_hook.rs) Deposits and burns tokens from sender to be minted on destination domain, and emits a crosschain message with additional hook data appended. In addition to the standard `deposit_for_burn` parameters, `deposit_for_burn_with_hook` accepts a dynamic-length `hookData` parameter, allowing the caller to include additional metadata to the attested message, which can be used to trigger custom logic on the destination chain. **Parameters** | Field | Type | Description | | :--------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `amount` | `u64` | Amount of tokens to deposit and burn. | | `destinationDomain` | `u32` | Destination domain identifier. | | `mintRecipient` | `Pubkey` | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* | | `destinationCaller` | `Pubkey` | Address which can call `receiveMessage` on destination domain. If set to `PublicKey.default`, any address can call `receiveMessage` *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* | | `maxFee` | `u64` | Max fee paid for fast burn, specified in units of the burn token. | | `minFinalityThreshold` | `u32` | Minimum finality threshold at which burn will be attested | | `hookData` | `Vec` | Additional metadata attached to the attested message, which can be used to trigger custom logic on the destination chain | #### [handleReceiveFinalizedMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/handle_receive_finalized_message.rs) Handles incoming message received by the local MessageTransmitter, and takes the appropriate action. For a burn message, mints the associated token to the requested recipient on the local domain. Validates the function sender is the local MessageTransmitter, and the remote sender is a registered remote TokenMessenger for `remoteDomain`. Additionally, reads the `feeExecuted` parameter from the BurnMessage. If nonzero, the `feeExecuted` amount is minted to the `feeRecipient`. **Parameters** | Field | Type | Description | | --------------------------- | -------------------------- | ------------------------------------------------------------ | | `remoteDomain` | `u32` | The domain where the message originated from | | `sender` | `Pubkey` | The sender of the message (remote TokenMessenger) | | `finalityThresholdExecuted` | `u32` | Specifies the level of finality Iris signed the message with | | `messageBody` | `Vec` (dynamic length) | The message body bytes | #### [handleReceiveUnfinalizedMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/handle_receive_unfinalized_message.rs) Handles incoming message received by the local MessageTransmitter, and takes the appropriate action. For a burn message, mints the associated token to the requested recipient on the local domain. Validates the function sender is the local MessageTransmitter, and the remote sender is a registered remote TokenMessenger for `remoteDomain`. Similar to `handleReceiveFinalizedMessage`, but is called for messages which are not finalized (`finalityThresholdExecuted` \< 2000). Unlike `handleReceiveFinalizedMessage`, `handleReceiveUnfinalizedMessage` has the following `messageBody` parameter: * **`expirationBlock`**. If `expirationBlock` ≤ `blockNumber` on the destination domain, the message will revert and must be re-signed without the expiration block. **Parameters** | Field | Type | Description | | --------------------------- | -------------------------- | --------------------------------------------------------------------------------- | | `remoteDomain` | `u32` | The domain where the message originated from | | `sender` | `Pubkey` | The sender of the message (remote TokenMessenger) | | `finalityThresholdExecuted` | `u32` | Specifies the level of finality Iris signed the message with | | `messageBody` | `Vec` (dynamic length) | The message body bytes (see [Message format](/cctp/technical-guide#message-body)) | ### MessageTransmitterV2 #### [`receiveMessage`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/message-transmitter-v2/src/instructions/receive_message.rs) Messages with a given nonce can only be broadcast successfully once for a pair of domains. The message body of a valid message is passed to the specified recipient for further processing. **Parameters** | Field | Type | Description | | :------------ | :-------- | :----------------------------- | | `message` | `Vec` | Message bytes. | | `attestation` | `Vec` | Signed attestation of message. | **Remaining Accounts** If the `receiveMessage` instruction is being called with a deposit for burn message that will be received by the `TokenMessengerMinterV2`, additional `remainingAccounts` are required so they can be passed with the CPI to `TokenMessengerMinter#handle_receive_finalized_message` or `TokenMessengerMinter#handle_receive_unfinalized_message`: | Account Name | PDA Seeds | PDA ProgramId | `isSigner`? | `isWritable`? | Description | | :------------------------------ | :---------------------------------------------------- | :------------------- | :---------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `token_messenger` | `["token_messenger"]` | tokenMessengerMinter | false | false | TokenMessenger Program Account | | `remote_token_messenger` | `["remote_token_messenger", sourceDomainId]` | tokenMessengerMinter | false | false | Remote token messenger account where the remote token messenger address is stored for the given source domain id | | `token_minter` | `["token_minter"]` | tokenMessengerMinter | false | true | TokenMinter Program Account | | `local_token` | `["local_token", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Local token account where the information for the local token (for example, USDCSOL) being minted is stored | | `token_pair` | `["token_pair", sourceDomainId, sourceTokenInBase58]` | tokenMessengerMinter | false | false | Token pair account where the info for the local and remote tokens are stored. `sourceTokenInBase58` is the remote token that was burned and converted into base58 format. | | `user_token_account` | N/A | N/A | false | true | User token account that will receive the minted tokens. This address **must** match the `mintRecipient` from the source chain `depositForBurn` call. | | `custody_token_account` | `["custody", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Custody account that holds the pre-minted USDCSOL that can be minted for CCTP usage. | | `SPL.token_program_id` | N/A | N/A | false | false | The native SPL token program ID. | | `token_program_event_authority` | `["__event_authority"]` | tokenMessengerMinter | false | false | Event authority account for the TokenMessengerMinter program. Needed to emit Anchor CPI events. | | `program` | N/A | N/A | false | false | Program id for the TokenMessengerMinter program. | #### [`sendMessage`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/message-transmitter-v2/src/instructions/send_message.rs) Sends a message to the destination domain and recipient. Stores message in a `MessageSent` account which will be attested by Circle's attestation service. **Parameters** | Field | Type | Description | | :------------------ | :-------- | :---------------------------------------------------- | | `destinationDomain` | `u32` | Destination domain identifier. | | `recipient` | `Pubkey` | Address to handle message body on destination domain. | | `messageBody` | `Vec` | App-specific message to be handled by recipient. | ## Additional Notes These notes are applicable to all CCTP versions. ### Mint Recipient for Solana as Destination Chain Transfers When calling `depositForBurn` on a non-Solana chain with Solana as the destination, the `mintRecipient` should be a **hex encoded USDC token account address**. The token account\* must exist at the time `receiveMessage` is called on Solana\* or else this instruction will revert. An example of converting an address from Base58 to hex taken from the Solana quickstart tutorial in TypeScript can be seen below: ```typescript TypeScript theme={null} import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; import { hexlify } from "ethers"; const solanaAddressToHex = (solanaAddress: string): string => hexlify(bs58.decode(solanaAddress)); ``` ### Mint Recipient for Solana as Source Chain Transfers When specifying the `mintRecipient` for Solana `deposit_for_burn` instruction calls, the address must be given as the 32 byte version of the hex address in base58 format. An example taken from the Solana quickstart tutorial in TypeScript can be seen below: ```typescript TypeScript theme={null} import { getBytes } from "ethers"; import { PublicKey } from "@solana/web3.js"; const evmAddressToBytes32 = (address: string): string => `0x000000000000000000000000${address.replace("0x", "")}`; const evmAddressToBase58PublicKey = (addressHex: string): PublicKey => new PublicKey(getBytes(evmAddressToBytes32(addressHex))); ``` ### Program Events Program events like [DepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L35-L45) , [MintAndWithdraw](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52) , and [MessageReceived](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52) are emitted as Anchor CPI events. This means a self-CPI is made into the program with the serialized event as instruction data so it is persisted in the transaction and can be fetched later on as needed. More information can be seen in the [Anchor implementation PR](https://github.com/coral-xyz/anchor/pull/2438), and an example of reading CPI events can be seen in the [`solana-cctp-contracts` repository](https://github.com/circlefin/solana-cctp-contracts/blob/master/tests/utils.ts#L62-L111). [MessageSent](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/events.rs#L49-L55) events are different, as they are stored in accounts. See the [MessageSent Event Storage section](#depositforburn) for more info. # CCTP Starknet Contracts and Interfaces Source: https://developers.circle.com/cctp/references/starknet-contracts Contracts for CCTP support on the Starknet blockchain ## Overview Starknet CCTP contracts are written in Cairo and run on a non-EVM zk-rollup. Transactions executed on Starknet are batched and proven using STARK proofs, which are then posted to Ethereum L1. This design allows Starknet to inherit Ethereum's security while offering higher throughput and lower fees. To align with Starknet's architecture while keeping parity with EVM chains, CCTP uses two contracts: * `TokenMessengerMinterV2`: consolidates the responsibilities of `TokenMessengerV2` (burn + send) and `TokenMinterV2` (receive + mint). * `MessageTransmitterV2`: provides the messaging layer that emits or receives attested messages and delivers them to `TokenMessengerMinterV2`. This mirrors how other non-EVM deployments (for example, Solana) combine messenger and minter logic while preserving behavior with the EVM equivalents. ## Mainnet contract addresses | Contract | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | :----------------------- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenMessengerMinterV2` | 25 | [`0x07d421B9cA8aA32DF259965cDA8ACb93F7599F69209A41872AE84638B2A20F2a`](https://voyager.online/contract/0x07d421B9cA8aA32DF259965cDA8ACb93F7599F69209A41872AE84638B2A20F2a) | | `MessageTransmitterV2` | 25 | [`0x02EBB5777B6dD8B26ea11D68Fdf1D2c85cD2099335328Be845a28c77A8AEf183`](https://voyager.online/contract/0x02EBB5777B6dD8B26ea11D68Fdf1D2c85cD2099335328Be845a28c77A8AEf183) | ## Testnet contract addresses | Contract | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | :----------------------- | :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenMessengerMinterV2` | 25 | [`0x04bDdE1E09a4B09a2F95d893D94a967b7717eB85A3f6dEcA8c080Ee01fBc3370`](https://sepolia.voyager.online/contract/0x04bDdE1E09a4B09a2F95d893D94a967b7717eB85A3f6dEcA8c080Ee01fBc3370) | | `MessageTransmitterV2` | 25 | [`0x04db7926C64f1f32a840F3Fa95cB551f3801a3600Bae87aF87807A54DCE12Fe8`](https://sepolia.voyager.online/contract/0x04db7926C64f1f32a840F3Fa95cB551f3801a3600Bae87aF87807A54DCE12Fe8) | ## CCTP interface * `TokenMessengerMinterV2`: initiates crosschain burns and mints tokens upon attested message receipt. * `MessageTransmitterV2`: emits messages, verifies attestations, and routes verified messages to the recipient contract. ### TokenMessengerMinterV2 interface The `TokenMessengerMinterV2` contract consolidates the roles of both `TokenMessengerV2` and `TokenMinterV2` found on EVM chains. It handles USDC burns, message emission, and token minting once crosschain messages are attested by Circle's Iris service. | Function | Description | Notes | | :----------------------------------- | :----------------------------------------------------------------------- | :---------------------------------------- | | `deposit_for_burn` | Burns USDC and emits a crosschain message for minting on another domain. | Standard CCTP transfer initiation. | | `deposit_for_burn_with_hook` | Same as `deposit_for_burn`, but attaches custom metadata (`hook_data`). | Used for programmable transfers. | | `handle_receive_finalized_message` | Mints USDC upon receiving a fully finalized message. | Called by `MessageTransmitterV2`. | | `handle_receive_unfinalized_message` | Processes partially finalized (“Fast Burn”) messages. | Enables faster crosschain transfers. | | `message_body_version` | Returns supported message format version. | Used for compatibility checks. | | `local_message_transmitter` | Returns the linked `MessageTransmitterV2` address. | Must match configured domain transmitter. | ### MessageTransmitterV2 interface The `MessageTransmitterV2` contract provides the core messaging layer for CCTP on Starknet. It is responsible for emitting, receiving, and validating crosschain messages, enforcing attestation rules, and ensuring message uniqueness. | Function | Description | Notes | | :-------------------------- | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | | `send_message` | Sends a crosschain message with specified domain, recipient, and message body. | Core function for outgoing CCTP messages. | | `receive_message` | Validates a message and its attestation; delivers message body to the recipient. | Called by an offchain forwarding service with an attestation from Circle to complete the transfer. | | `get_max_message_body_size` | Returns the maximum allowed message size. | Used by offchain components for validation. | | `is_nonce_used` | Checks if a message nonce has been processed already. | Prevents message replay. | | `get_local_domain` | Returns this contract's domain ID. | Expected to be 25 for Starknet. | | `get_version` | Returns protocol version supported by this transmitter. | Used by Iris attestation service. | # CCTP on Stellar Source: https://developers.circle.com/cctp/references/stellar Learn how to send funds to Stellar addresses and how CCTP handles Stellar USDC. CCTP on Stellar has two behaviors you must account for when integrating: a 32-byte address format that does not distinguish accounts from contracts, and a seven-decimal USDC precision that differs from other CCTP supported blockchains. See [CCTP Stellar Contracts and Interfaces](/cctp/references/stellar-contracts) for the contract addresses and interfaces. Always [use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and `destinationCaller` to the `CctpForwarder` [contract address](/cctp/references/stellar-contracts). * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is set to a user account or muxed address, USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. ## Stellar address types On Stellar, addresses are `strkey` strings composed of a type identifier and a 32-byte payload. The type identifier determines the account type: user accounts (`G`) carry an Ed25519 public key, contracts (`C`) carry a contract ID hash. Muxed accounts (`M`) are a type of `G` account that additionally embeds a numeric identifier alongside the Ed25519 public key (see Stellar's [muxed accounts documentation](https://developers.stellar.org/docs/build/guides/transactions/pooled-accounts-muxed-accounts-memos#muxed-accounts)). CCTP messages store only the raw 32-byte payload without the type identifier, so the protocol cannot distinguish between address types and assumes the `mintRecipient` is always a contract. Use `CctpForwarder` when transferring to Stellar to ensure funds are forwarded to the intended recipient. ## Use `CctpForwarder` for Stellar recipients `CctpForwarder` is a publicly callable onchain contract that receives minted USDC on Stellar and atomically forwards it to `forwardRecipient`. Encode `forwardRecipient` in hook data as a Stellar `strkey`. The prefix `G`, `M`, or `C` identifies the recipient address type. On the source burn, both `mintRecipient` and `destinationCaller` must be set to the `CctpForwarder` [contract address](/cctp/references/stellar-contracts). * If `destinationCaller` is wrong, the forwarder cannot complete the transfer. * If `mintRecipient` is set to a user account or muxed address, USDC is not sent to the forwarder. In either case, funds become permanently stuck and **cannot be recovered**. ### How it works Call `mint_and_forward` on `CctpForwarder` through the Stellar Soroban client for your language. Pass the raw CCTP message and attestation bytes. The following shows the onchain contract interface. It is not a TypeScript or JavaScript function you call directly. Your Soroban client builds an `invokeHostFunction` operation from these arguments. ```text theme={null} mint_and_forward(message: Bytes, attestation: Bytes) ``` For TypeScript, [`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) documents how to encode arguments and how to simulate, sign, and submit transactions against [Stellar RPC](https://developers.stellar.org/docs/data/rpc). Inside `mint_and_forward`, `CctpForwarder` does the following: 1. Validates the message. 2. Extracts `forwardRecipient` from hook data. 3. Calls `receive_message` on `MessageTransmitter`, which mints USDC to `CctpForwarder`. 4. Transfers the minted USDC to `forwardRecipient`. 5. Runs atomically. Any failure reverts the invocation. The `CctpForwarder` flow is non-custodial. The mint and the payout to `forwardRecipient` both run onchain in that single Soroban invocation. Circle does not take custody of the minted balance in between. ### Hook format The hook data begins with the reserved magic bytes, followed by versioning and payload fields. On Stellar, bytes 28 onward carry the length of `forwardRecipient`, the `forwardRecipient` `strkey`, and any optional trailing bytes for integrator use. | Bytes | Type | Data | | -------------- | --------- | --------------------------------------------------- | | 0-23 | `bytes24` | Magic. Circle-reserved bytes; use all zero bytes | | 24-27 | `uint32` | Version; set to `0` | | 28-31 | `uint32` | `L`: length of `forwardRecipient` in bytes | | `32..(32+L-1)` | `bytes` | `forwardRecipient` as a `strkey` | | `(32+L)..` | `bytes` | Optional integrator-defined payload; omit if unused | #### Building forwarder hook data (example) The following helper functions validate Stellar contract `strkey` inputs and build the `hookData` payload for an EVM `depositForBurnWithHook` call: ```ts TypeScript theme={null} import { StrKey } from "@stellar/stellar-sdk"; /** * Validates that the input is a Stellar contract address (C…) and decodes it * to a 0x-prefixed bytes32 hex string suitable for EVM contract calls. * * @param strkey - Stellar contract address (C…) * @returns 0x-prefixed 64-character hex string * @throws If the input is not a valid contract address */ function contractStrkeyToBytes32(strkey: string): `0x${string}` { if (!StrKey.isValidContract(strkey)) { throw new Error(`Invalid contract strkey: ${strkey}`); } return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`; } /** * Builds the hookData buffer for a CCTP Forwarder burn message. * * Hook data layout: * bytes 0–23: reserved (zeroed) * bytes 24–27: hook data version (u32 BE, currently 0) * bytes 28–31: forward_recipient byte length (u32 BE) * bytes 32+ : forward_recipient (UTF-8 encoded Stellar strkey) * * @param forwardRecipientStrkey - Stellar strkey of the final token recipient (C…, G…, or M…) * @returns Hook data as a 0x-prefixed hex string */ function buildCctpForwarderHookData( forwardRecipientStrkey: string, ): `0x${string}` { const isValid = StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) || StrKey.isValidContract(forwardRecipientStrkey) || StrKey.isValidMed25519PublicKey(forwardRecipientStrkey); if (!isValid) { throw new Error( `Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`, ); } const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8"); const hookData = Buffer.alloc(32 + recipientBytes.length); hookData.writeUInt32BE(0, 24); // hook version = 0 hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8 return `0x${hookData.toString("hex")}`; } interface DepositForBurnWithHookParams { amount: bigint; destinationDomain: number; mintRecipient: `0x${string}`; burnToken: `0x${string}`; destinationCaller: `0x${string}`; maxFee: bigint; minFinalityThreshold: number; hookData: `0x${string}`; } /** * Prepares all arguments for an EVM `depositForBurnWithHook` call targeting * Stellar via the CCTP Forwarder. Converts Stellar strkeys to 0x-prefixed * bytes32 hex strings and encodes the hook data. * * @param amount - Token amount to burn (in EVM token decimals) * @param cctpForwarderStrkey - Stellar strkey of the CCTP Forwarder contract (C…), used as mintRecipient and destinationCaller * @param burnToken - EVM address of the token to burn * @param maxFee - Maximum fee for the burn * @param minFinalityThreshold - Minimum finality threshold (1000 = fast, 2000 = standard) * @param forwardRecipientStrkey - Stellar strkey of the final token recipient (C…, G…, or M…), encoded in hookData */ function prepareEvmDepositForBurnWithHookToStellar( amount: bigint, cctpForwarderStrkey: string, burnToken: `0x${string}`, maxFee: bigint, minFinalityThreshold: number, forwardRecipientStrkey: string, ): DepositForBurnWithHookParams { const cctpForwarderHex = contractStrkeyToBytes32(cctpForwarderStrkey); const hookData = buildCctpForwarderHookData(forwardRecipientStrkey); return { amount, destinationDomain: 27, mintRecipient: cctpForwarderHex, burnToken, destinationCaller: cctpForwarderHex, maxFee, minFinalityThreshold, hookData, }; } ``` ## Stellar addresses in CCTP messages and API responses [Stellar addresses](#stellar-address-types) are `strkey` strings. CCTP message fields store only 32-byte address payloads. They omit the `strkey` encoding, including the `G`, `M`, or `C` type marker, so the raw bytes in the message do not say whether the address is an account or a contract. `mintRecipient` is always assumed to be a contract address. You must [use `CctpForwarder`](#use-cctpforwarder-for-stellar-recipients) to make transfers to Stellar. ### CCTP message fields The following tables describe each address field in the CCTP message, explain how Stellar uses it during a mint (inbound) or burn (outbound), and indicate whether you need to design around the address type. For the full message layout, see the [CCTP Technical Guide](/cctp/references/technical-guide#message-format). #### Inbound transfers to Stellar destination | Field | Operation | Must design around address type? | | ------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `sender` | Validate against the source domain `TokenMessenger` mapping. | No | | `recipient` | Select the Stellar contract that handles the destination `receive_message`. | No, always a contract (`C`) | | `destinationCaller` | Restrict who may call `receive_message` (`require_auth` compares bytes). | No, compare raw bytes | | `burnToken` | Map the burned token identifier to Stellar USDC. | No, known asset contract | | `messageSender` | Not used operationally on Stellar. | No | | `mintRecipient` | Mint USDC to this 32-byte destination on Stellar. | Yes, always assumed to be a contract; [use `CctpForwarder` for Stellar recipients](#use-cctpforwarder-for-stellar-recipients) | #### Outbound transfers from Stellar source | Field | Operation | Must design around address type? | | ------------------- | ----------------------------------------------------------------------------------------- | -------------------------------- | | `sender` | 32 byte address payload of the Stellar `TokenMessengerMinterV2` used to perform the burn. | No | | `burnToken` | Identify the Stellar USDC contract that is burned. | No | | `mintRecipient` | Encode the recipient on the destination blockchain. | No | | `messageSender` | Record caller context (not used operationally on Stellar). | No | | `destinationCaller` | Encode which address may call receive on the destination blockchain. | No | | `recipient` | Encode the handler contract on the destination blockchain. | No | ### Null address fields in API responses When a CCTP message involves Stellar, the [Get messages](/api-reference/cctp/all/get-messages-v2) endpoint returns all address fields in `decodedMessage` and `decodedMessageBody` as `null` because the API cannot distinguish a 32-byte Stellar account from a contract. To read those addresses, parse the raw hex in the `message` field directly. The following example shows an Ethereum-to-Stellar transfer response with typical `null` address fields: ```json JSON theme={null} { "messages": [ { "message": "0x...", "eventNonce": "0", "attestation": "0x...", "cctpVersion": 2, "status": "complete", "decodedMessage": { "sourceDomain": "0", "destinationDomain": "27", "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000", "sender": null, "recipient": null, "destinationCaller": null, "minFinalityThreshold": "1000", "finalityThresholdExecuted": "2000", "messageBody": "0x...", "decodedMessageBody": { "burnToken": null, "mintRecipient": null, "amount": "1000000", "messageSender": null, "maxFee": "0", "feeExecuted": "0", "expirationBlock": "0", "hookData": null } } } ] } ``` ## USDC precision for CCTP and Stellar Stellar represents USDC in seven-decimal subunits while other CCTP-supported blockchains use six. How CCTP handles that difference depends on whether Stellar is the source or destination blockchain. Regardless of direction, the `amount` field in a CCTP message is always in six-decimal subunits. Stellar wallets and SDKs often display seven fractional digits. Use six-decimal subunits in `amount` when handling CCTP messages offchain. ### Stellar as the source When Stellar is the source blockchain, the burn debits only through the sixth decimal digit of the user's balance. Anything in the seventh decimal place stays in the user's account. 1. A user bridges **0.1234567 USDC** from Stellar to the destination blockchain. 2. Stellar burns **0.1234560 USDC**. 3. **0.0000007 USDC** stays in the user's Stellar account. 4. The CCTP message `amount` is **123456** (six-decimal subunits). 5. The destination blockchain mints **0.123456 USDC** to the recipient. ### Stellar as the destination When Stellar is the destination blockchain, the mint converts the six-decimal message `amount` into seven by scaling the integer by 10 (for example, `123456` becomes `1234560` seven-decimal subunits). 1. A user bridges **0.123456 USDC** from the source blockchain to Stellar. 2. The CCTP message `amount` is **123456** (six-decimal subunits). 3. Stellar mints **0.1234560 USDC** to the recipient. # CCTP Stellar Contracts and Interfaces Source: https://developers.circle.com/cctp/references/stellar-contracts Contracts for CCTP support on the Stellar network ## Overview Stellar CCTP contracts run on Soroban, Stellar's smart contracts platform. CCTP message fields use 32-byte address encodings. CCTP treats `mintRecipient` as a contract address. If the recipient is a Stellar user or [muxed](https://developers.stellar.org/docs/build/guides/transactions/pooled-accounts-muxed-accounts-memos#muxed-accounts) account instead, hook data can carry a `forwardRecipient` `strkey` so the forwarder can send funds to that address. To align with Stellar address encoding while keeping parity with EVM and other non-EVM blockchains, CCTP uses three contracts: * `TokenMessengerMinter`: consolidates the responsibilities of `TokenMessengerV2` (burn + send) and `TokenMinterV2` (receive + mint). On mint, `mintRecipient` is treated as a contract address and hooks supply the `forwardRecipient` when needed. * `MessageTransmitter`: provides the messaging layer that emits or receives attested messages and delivers them to `TokenMessengerMinter` (including `receive_message` for forwarder flows). * `CctpForwarder`: receives minted USDC and forwards it to `forwardRecipient` in hook data. ## Mainnet contract addresses | Contract | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | :--------------------- | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenMessengerMinter` | 27 | [`CAE2G5Z77UP7GYPYGFOWFGW7C7J6I4YP2AFGSADRKQY62SYUFLPNFTXL`](https://stellar.expert/explorer/public/contract/CAE2G5Z77UP7GYPYGFOWFGW7C7J6I4YP2AFGSADRKQY62SYUFLPNFTXL) | | `MessageTransmitter` | 27 | [`CACMENFFJPJMSDAJQLX4R7K3SFZIW2LJSE3R2UMLGSWHFHS353FVXAZV`](https://stellar.expert/explorer/public/contract/CACMENFFJPJMSDAJQLX4R7K3SFZIW2LJSE3R2UMLGSWHFHS353FVXAZV) | | `CctpForwarder` | 27 | [`CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T`](https://stellar.expert/explorer/public/contract/CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T) | ## Testnet contract addresses | Contract | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | :--------------------- | :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenMessengerMinter` | 27 | [`CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP`](https://stellar.expert/explorer/testnet/contract/CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP) | | `MessageTransmitter` | 27 | [`CBJ6MTCKKZG73PMDZCJMSFRD7DQEMI4FKDH7CGDSV4W6FHCRBCQAVVJY`](https://stellar.expert/explorer/testnet/contract/CBJ6MTCKKZG73PMDZCJMSFRD7DQEMI4FKDH7CGDSV4W6FHCRBCQAVVJY) | | `CctpForwarder` | 27 | [`CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ`](https://stellar.expert/explorer/testnet/contract/CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ) | ## CCTP interface * `TokenMessengerMinter`: initiates crosschain burns and mints tokens upon attested message receipt. * `MessageTransmitter`: emits messages, verifies attestations, and routes verified messages to the recipient contract. * `CctpForwarder`: completes mint and forward in one transaction when hook data supplies a `forwardRecipient` `strkey`. ### TokenMessengerMinter interface The `TokenMessengerMinter` contract consolidates the roles of both `TokenMessengerV2` and `TokenMinterV2` found on EVM chains. It handles USDC burns, message emission, and token minting once crosschain messages are attested by Circle's Iris service. On Stellar it assumes `mintRecipient` is a contract. Account recipients use `CctpForwarder` and hook-qualified `forwardRecipient` bytes. | Function | Description | Notes | | :----------------------------------- | :----------------------------------------------------------------------- | :------------------------------------------------------ | | `deposit_for_burn` | Burns USDC and emits a crosschain message for minting on another domain. | Standard CCTP transfer initiation. | | `deposit_for_burn_with_hook` | Same as `deposit_for_burn`, but attaches custom metadata (`hook_data`). | Used for programmable transfers and Stellar forwarding. | | `handle_receive_finalized_message` | Mints USDC upon receiving a fully finalized message. | Called by `MessageTransmitter`. | | `handle_receive_unfinalized_message` | Processes partially finalized ("Fast Burn") messages. | Enables faster crosschain transfers. | | `message_body_version` | Returns supported message format version. | Used for compatibility checks. | | `local_message_transmitter` | Returns the linked `MessageTransmitter` address. | Must match configured domain transmitter. | ### MessageTransmitter interface The `MessageTransmitter` contract provides the core messaging layer for CCTP on Stellar. It is responsible for emitting, receiving, and validating crosschain messages, enforcing attestation rules, and ensuring message uniqueness. | Function | Description | Notes | | :-------------------------- | :------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | | `send_message` | Sends a crosschain message with specified domain, recipient, and message body. | Core function for outgoing CCTP messages. | | `receive_message` | Validates a message and its attestation; delivers message body to the recipient. | Called by an offchain forwarding service (or by `CctpForwarder` in the forwarder flow) with an attestation from Circle. | | `get_max_message_body_size` | Returns the maximum allowed message size. | Used by offchain components for validation. | | `is_nonce_used` | Checks if a message nonce has been processed already. | Prevents message replay. | | `get_local_domain` | Returns this contract's domain ID. | Expected to be 27 for Stellar. | | `get_version` | Returns protocol version supported by this transmitter. | Used by Iris attestation service. | ### CctpForwarder interface The `CctpForwarder` contract calls `receive_message` on `MessageTransmitter`, takes the mint, and transfers USDC to `forwardRecipient` parsed from `hook_data`. See [Hook format](/cctp/references/stellar#hook-format) for details. | Function | Description | Notes | | :----------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | | `mint_and_forward(message: Bytes, attestation: Bytes)` | Verifies message and attestation. Runs `receive_message` so USDC mints to `CctpForwarder`. Sends USDC to `forwardRecipient` from hooks. | Atomic, any failure reverts. | The `CctpForwarder` flow is non-custodial. `mint_and_forward` mints to this contract and pays `forwardRecipient` in one atomic Soroban invocation. Circle does not take custody of the minted balance in between. # CCTP Technical Guide Source: https://developers.circle.com/cctp/references/technical-guide Technical explainer for CCTP ## Message passing Cross-Chain Transfer Protocol (CCTP) uses generalized message passing to facilitate the native burning and minting of USDC across supported blockchains, also known as [domains](/cctp/cctp-supported-blockchains#cctp-supported-domains). Message passing is a three-step process: 1. An onchain component on the source domain emits a message. 2. Circle's offchain attestation service signs the message. 3. The onchain component at the destination domain receives the message, and forwards the message body to the specified recipient. Onchain components serve the same purpose across all domains, but their implementations differ between EVM-compatible and non-EVM domains. Moreover, there are both implementation and naming differences between CCTP V2 and previous versions due to the addition of Fast Transfer and other improvements. ### For EVM chains The relationship between CCTP's onchain components and Circle's offchain Attestation Service is illustrated below for a burn-and-mint of USDC between EVM-compatible domains: On EVM domains, the onchain component for crosschain burning and minting is called **TokenMessengerV2**, which is built on top of **MessageTransmitterV2**, an onchain component for generalized message passing. In the diagram, a token depositor calls the [TokenMessengerV2#depositForBurn](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/TokenMessengerV2.sol#L158) function to deposit a native token (such as USDC), which delegates to the TokenMinterV2 contract to burn the token. The **TokenMessengerV2** contract then sends a message via the [MessageTransmitterV2#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L143) function. After [sufficient block confirmations](/cctp/required-block-confirmations), Circle's offchain attestation service, Iris, signs the message. An API consumer must query this attestation and submits it onchain to the destination domain's [MessageTransmitterV2#receiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L206) function. To send an arbitrary message, directly call [MessageTransmitterV2#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L143). The message recipient must implement the following methods to handle messages based on their finality threshold: * Implement [IMessageHandlerV2#handleReceiveFinalizedMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/interfaces/v2/IMessageHandlerV2.sol#L35) to receive messages with `finalityThresholdExecuted` ≥ 2000. * Implement [IMessageHandlerV2#handleReceiveUnfinalizedMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/interfaces/v2/IMessageHandlerV2.sol#L51) to receive messages with `finalityThresholdExecuted` \< 2000. This distinction allows the recipient to control the level of finality it requires before accepting a message. ### For non-EVM chains CCTP is also available on several non-EVM blockchains where USDC is natively issued, extending crosschain capabilities to the broader ecosystem. On Stellar, USDC precision and address encoding differ from other CCTP-supported blockchains. For inbound transfers, use [`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients) so funds reach the correct recipient. See [CCTP on Stellar](/cctp/references/stellar). ## Message format ### Message header The top-level message header format is standard for all messages passing through CCTP. | Field | Offset | Solidity Type | Length (bytes) | Description | | --------------------------- | ------ | ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `version` | 0 | `uint32` | 4 | Version identifier - use 1 for CCTP | | `sourceDomain` | 4 | `uint32` | 4 | Source domain ID | | `destinationDomain` | 8 | `uint32` | 4 | Destination domain ID | | `nonce` | 12 | `bytes32` | 32 | Unique message nonce (see [CCTP V2 Nonces](#cctp-v2-nonces)) | | `sender` | 44 | `bytes32` | 32 | Address of MessageTransmitterV2 caller on source domain | | `recipient` | 76 | `bytes32` | 32 | Address to handle message body on destination domain | | `destinationCaller` | 108 | `bytes32` | 32 | Address permitted to call MessageTransmitterV2 on destination domain, or bytes32(0) if message can be received by any address | | `minFinalityThreshold` | 140 | `uint32` | 4 | Minimum finality threshold before allowed to attest (see [CCTP V2 Finality Thresholds](#cctp-v2-finality-thresholds)) | | `finalityThresholdExecuted` | 144 | `uint32` | 4 | Actual finality threshold executed from source chain (see [CCTP V2 Finality Thresholds](#cctp-v2-finality-thresholds)) | | `messageBody` | 148 | `bytes` | dynamic | App-specific message to be handled by recipient | #### Nonces A CCTP nonce is a unique identifier for a message that can only be used once on the destination domain. Circle assigns CCTP nonces offchain. The nonce for each message in a transaction can be queried through the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint, using the transaction hash as a query parameter. **Why `bytes32` type for addresses** CCTP is built to support EVM chains, which use 20 byte addresses, and non-EVM chains, many of which use 32 byte addresses. Circle provides a [`Message.sol` library](https://github.com/circlefin/evm-cctp-contracts/blob/40111601620071988e94e39274c8f48d6f406d6d/src/messages/Message.sol#L145-L157) as a reference implementation for converting between address and `bytes32` in Solidity. ### Message body The message format includes a dynamically sized `messageBody` field, used for application-specific messages. For example, `TokenMessengerV2` defines a [BurnMessageV2](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol) with data related to crosschain transfers. | Field | Offset | Solidity Type | Length (bytes) | Description | | ----------------- | ------ | ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `version` | 0 | `uint32` | 4 | Version identifier - use 1 for CCTP | | `burnToken` | 4 | `bytes32` | 32 | Address of burned token on source domain | | `mintRecipient` | 36 | `bytes32` | 32 | Address to receive minted tokens on destination domain | | `amount` | 68 | `uint256` | 32 | Amount of burned tokens | | `messageSender` | 100 | `bytes32` | 32 | Address of caller of `depositForBurn` (or `depositForBurnWithCaller`) on source domain | | `maxFee` | 132 | `uint256` | 32 | Maximum fee to pay on the destination domain, specified in units of `burnToken` | | `feeExecuted` | 164 | `uint256` | 32 | Actual fee charged on the destination domain, specified in units of `burnToken` (capped by `maxFee`) | | `expirationBlock` | 196 | `uint256` | 32 | An expiration block 24 hours in the future is encoded in the message before signing by attestation service, and is respected on the destination chain. If the burn expires, it must be re-signed. Expiration acts as a safety mechanism against problems with finalization, such as a stuck sequencer. | | `hookData` | 228 | `bytes` | dynamic | Arbitrary data to be included in the `depositForBurn` on source domain and to be executed on destination domain | **`expirationBlock` on ARB-stack blockchains** For ARB-stack destination blockchains (Arbitrum, EDGE, and Plume), the `expirationBlock` is an Ethereum (L1) block number, not the L2 block number. ARB-stack blockchains track blocks internally using the parent blockchain (Ethereum). When validating expiration for these blockchains, compare the `expirationBlock` value against the current Ethereum block number, not the L2 block number. ## API hosts and endpoints CCTP provides a set of API hosts and endpoints to manage messages, attestations, and transaction details for your crosschain USDC transfers. ### API service hosts | Environment | URL | | :---------- | :------------------------------------ | | **Testnet** | `https://iris-api-sandbox.circle.com` | | **Mainnet** | `https://iris-api.circle.com` | **API Service Rate Limit** The CCTP API service rate limit is 35 requests per second. If you exceed 35 requests per second, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 response. ### API endpoints CCTP endpoints enable advanced capabilities such as fetching attestations for **Standard Transfer** or **Fast Transfer** burn events, verifying public keys across versions, accessing transaction details, querying fast transfer allowances and fees, and initiating re-attestation processes. Below is an overview of the CCTP public endpoints. Click on any endpoint for its API reference. | Endpoint | Description | Use Case | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | [`GET /v2/publicKeys`](/api-reference/cctp/all/get-public-keys-v2) | Returns public keys for validating attestations across all supported CCTP versions. | Retrieve public keys to verify attestation authenticity for crosschain transactions. | | [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) | Retrieves messages and attestations for a given transaction or nonce, supporting messages for all CCTP versions. | Fetch attestation status and transaction details. | | [`POST /v2/reattest`](/api-reference/cctp/all/reattest-message) | Re-attests a soft finality V2 message to achieve finality or revive expired Fast Transfer burns. | Handle edge cases requiring updated attestations or finalize transactions with stricter rules. | | [`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance) | Retrieves the current USDC Fast Transfer allowance remaining. | Monitor available allowance for Fast Transfer burns in real-time. | | [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) | Returns the fees for USDC transfers between specified source and destination domains. | Calculate transaction costs before initiating a Fast or Standard Transfer. | **Deprecated endpoint** The endpoint `/v2/fastBurn/USDC/fees` is deprecated. Use [`/v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) instead to retrieve both Fast and Standard Transfer fees. **Note:** This deprecation does **not** affect [`/v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance) (see preceding table), which remains active and valid. ## Finality thresholds CCTP has the concept of a finality threshold, which is a chain-agnostic representation of the confirmation level required before an attestation is issued. This allows integrators to specify how many confirmations are needed based on their risk tolerance or use case. In CCTP, each message specifies a `minFinalityThreshold`. This threshold indicates the minimum level of confirmation required for Circle's attestation service (Iris) to attest to the message. Iris will not attest to a message at a confirmation level below the specified minimum threshold. This allows applications to enforce a desired level of finality before acting on an attestation on the destination chain. ### Defined finality thresholds CCTP V2 defines the following finality thresholds: | Finality Threshold | Value | | ------------------ | ----- | | **Confirmed** | 1000 | | **Finalized** | 2000 | ### Messages and finality * Messages with a `minFinalityThreshold` of **1000** or lower are considered **Fast** messages. These messages are eligible for fast attestation at the *confirmed* level by Iris. * Messages with a `minFinalityThreshold` of **2000** are considered **Standard** messages. These messages are attested to at the *finalized* level by Iris. Only two finality thresholds are supported. Any `minFinalityThreshold` value below **1000** is treated as **1000**, and any value above **1000** is treated as **2000**. ## Fees For information about CCTP transfer fees, including fee tables by blockchain, the `maxFee` parameter, and Standard Transfer fee switch support, see [CCTP Fees](/cctp/concepts/fees). ## Hooks Hooks in CCTP V2 are metadata that can be attached to a burn message, allowing integrators to execute custom logic at the destination chain. Hook execution is left entirely to the integrator, offering maximum flexibility and enabling broader crosschain compatibility without altering the core CCTP protocol. ### Design overview CCTP does not implement hook execution in the core protocol. Instead, hooks are treated as opaque metadata passed along with the burn message. This design allows integrators to define and control how hooks are processed on the destination chain, based on their own infrastructure and trust model. ### Key benefits * **Maximum flexibility for integrators** * Determine execution timing: pre-mint or post-mint * Implement custom recovery or error-handling strategies if hook execution fails * Choose any execution environment (EVM or non-EVM); even non-EVM chains can support Hooks as data passed into a function call. * **Improved Compliance and Security Separation** * **Compliance**: By delegating hook execution to the integrator, the protocol maintains a clear boundary between CCTP's core message-passing capabilities and application-specific logic. This modular approach helps integrators meet their own compliance requirements with greater flexibility. * **Security**: By keeping hook execution outside the core protocol, CCTP maintains a smaller and more focused security surface, while allowing integrators to manage their own execution environments independently. ## Security audit The CCTP smart contracts have been independently audited by two third-party security firms: * CCTP was audited by [ChainSecurity (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/ChainSecurity_Circle_CCTP_V2_audit%20\(1\).pdf) and [OtterSec (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/public_evm_cctp_audit_final%20\(2\).pdf) * CCTP (with Standard Transfer fee switch) was audited by [ChainSecurity (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/CCTP/ChainSecurity_Circle_CCTP_audit_2025-07.pdf) # Cross-Chain Transfer Protocol V1 Source: https://developers.circle.com/cctp/v1 Move USDC securely across blockchains and simplify user experience **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Overview **Cross-Chain Transfer Protocol V1** is a permissionless onchain utility that facilitates USDC transfers securely between blockchain networks via native burning and minting. Circle created CCTP to improve capital efficiency and minimize trust requirements when using USDC across blockchain networks. CCTP V1 enables developers to build multichain applications that allow users to perform 1:1 transfers of USDC securely across blockchains. **Note:** CCTP V1 only supports **Standard Transfer**, constrained by blockchain finality on the source blockchain. Later [CCTP](/cctp) versions support **Fast Transfer** and **Hooks**, in addition to also supporting **Standard Transfer**. ## Understanding the Problem Blockchain networks often operate in siloed environments and cannot natively communicate with one another. While some ecosystems, such as Cosmos, use built-in protocols like the Inter-Blockchain Communication (IBC) protocol to enable data transmission between their appchains, direct communication between isolated networks, such as Ethereum and Avalanche, remains infeasible. Traditional bridges exist to address this limitation by enabling the transfer of digital assets, such as USDC, across blockchains. However, these bridges come with significant drawbacks. Two common methods, lock-and-mint bridging and liquidity pool bridging, require locking USDC liquidity in third-party smart contracts. This approach reduces capital efficiency and introduces additional trust assumptions. ## Design Approach As a low-level primitive, CCTP V1 can be embedded within any app or wallet - even existing bridges - to enhance and simplify the user experience for cross-chain use cases. With USDC circulating across a large number of blockchain networks, CCTP V1 can connect and unify liquidity across disparate ecosystems where it's supported. CCTP V1 is built on generalized message passing and designed for composability, enabling a wide range of use cases. Developers can extend its functionality beyond just moving USDC between blockchains. For example, you can create a flow where USDC is sent across chains and automatically deposited into a DeFi lending pool after the transfer, allowing it to generate yield in an automated manner. This experience can be designed to feel like a seamless, single transaction for the end user. ## How CCTP V1 works **Standard Transfer** is the default method in CCTP V1 for transferring USDC across blockchains, which involves burning USDC on the source chain and minting it on the destination chain. It relies on transaction finality on the source chain and uses Circle's Attestation Service to enable standard-finality (hard finality) transfers. The process includes the following steps: 1. **Initiation**. A user accesses an app powered by CCTP V1 and initiates a Standard Transfer of USDC, specifying the recipient's wallet address on the destination chain. 2. **Burn Event**. The app facilitates a burn of the specified USDC amount on the source blockchain. 3. **Attestation**. Circle's Attestation Service observes the burn event and, after observing hard finality on the source chain, issues a signed attestation. Hard finality ensures the burn is irreversible (about 13 to 19 minutes for Ethereum and L2 chains.) 4. **Mint Event**. The app retrieves the signed attestation from Circle and uses it to mint USDC on the destination chain. 5. **Completion**. The recipient wallet address receives the newly minted USDC on the destination blockchain, completing the transfer. **Standard Transfer** prioritizes reliability and security, making it suitable for scenarios where finality wait times are acceptable. ## Use Cases CCTP V1 enables developers to build novel cross-chain apps that integrate functionalities like trading, lending, payments, NFTs, and gaming, while simplifying the user experience. Below are some practical examples of how you can leverage CCTP V1 in your applications, either directly or indirectly by routing USDC behind the scenes: ### Cross-chain rebalancing Market makers, fillers/solvers, exchanges, and bridges can use CCTP V1 to manage liquidity more efficiently. By securely rebalancing USDC holdings across blockchains, you can reduce operational costs, meet demand, and take advantage of market opportunities with minimal latency. ### Cross-chain swaps With CCTP V1, users can quickly swap between digital assets on different blockchains by routing through USDC. Users can also swap for USDC and automatically trigger subsequent actions on the destination chain, enabling seamless cross-chain transactions. ### Cross-chain purchases Automate cross-chain purchases with CCTP V1. For example, a user can use USDC on one chain to purchase an NFT on a decentralized exchange on another chain and list it for sale on an NFT marketplace. When the transaction is initiated, CCTP V1 routes USDC across chains to buy the NFT and opens the listing on the marketplace—all in one streamlined flow. ### Simplify cross-chain complexities Simplify the cross-chain experience by using USDC as collateral on one chain to open a borrowing position on a lending protocol on another chain. With CCTP V1, USDC can move quickly between blockchains, allowing users to onboard to new applications without switching wallets or managing multi-chain complexities. # CCTP Aptos Packages and Interfaces V1 Source: https://developers.circle.com/cctp/v1/aptos-packages Packages for CCTP V1 support on the Aptos blockchain **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. ## Overview The CCTP V1 Aptos smart contract implementation is written in [Move](https://aptos.dev/en/build/smart-contracts). The Aptos CCTP V1 implementation is split into two packages: `MessageTransmitter` and `TokenMessengerMinter`. `TokenMessengerMinter` encapsulates the functionality of both `TokenMessenger` and `TokenMinter` contracts on EVM chains. To ensure alignment with EVM contracts logic and state, and to facilitate future upgrades and maintenance, the code and state of the Aptos packages reflect the EVM counterparts as closely as possible. The key difference with Aptos packages from EVM and other CCTP V1 implementations is the receive message flow. Since the Move language uses static dispatch and requires all dependencies to be available at compile time, the `MessageTransmitter` `receive_message` function cannot call into the receiver package (e.g. `TokenMessengerMinter` for USDC transfers). The workaround for this limitation is that callers of `message_transmitter::receive_message` must also atomically (in the same transaction or [move script](https://aptos.dev/en/build/smart-contracts/scripts)) call into the receiver package's `handle_receive_message` function with a Receipt object returned from `receive_message`, and then pass a `Receipt` object back into the `message_transmitter::complete_receive_message` function to complete the message and destroy the `Receipt` object. Please see the interface and examples below for more information on this flow. Below are the known package IDs and object IDs on Aptos testnet and mainnet. ### Testnet #### Package IDs | Package | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :------------------------------------------------------------------- | | MessageTransmitter | 9 | `0x081e86cebf457a0c6004f35bd648a2794698f52e0dde09a48619dcd3d4cc23d9` | | TokenMessengerMinter | 9 | `0x5f9b937419dda90aa06c1836b7847f65bbbe3f1217567758dc2488be31a477b9` | #### Object IDs | Object | Object ID | | :------------------- | :------------------------------------------------------------------- | | MessageTransmitter | `0xcbb70e4f5d89b4a37e850c22d7c994e32c31e9cf693e9633784e482e9a879e0c` | | TokenMessengerMinter | `0x1fbf4458a00a842a4774f441fac7a41f2da0488dd93a43880e76d58789144e17` | | Stablecoin | `0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832` | CCTP V1 and Stablcoin use a shared package `AptosExtensions` deployed at `0xb75a74c6f8fddb93fdc00194e2295d8d5c3f6a721e79a2b86884394dcc554f8f` ### Mainnet #### Package IDs | Package | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :------------------------------------------------------------------- | | MessageTransmitter | 9 | `0x177e17751820e4b4371873ca8c30279be63bdea63b88ed0f2239c2eea10f1772` | | TokenMessengerMinter | 9 | `0x9bce6734f7b63e835108e3bd8c36743d4709fe435f44791918801d0989640a9d` | #### Object IDs | Object | Object ID | | :------------------- | :------------------------------------------------------------------- | | MessageTransmitter | `0x45bf7f71e44750f2b2a7a1fea21fc44b4a83ba5d68ab10c7a3935f6d8cbdbc75` | | TokenMessengerMinter | `0x9e6702a472080ea3caaf6ba9dfaa6effad2290a9ba9adaacd5af5c618e42782d` | | Stablecoin | `0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b` | The shared package `AptosExtensions` is deployed at `0x98bce69c31ee2cf91ac50a3f38db7b422e3df7cdde9fe672ee1d03538a6aeae0` ## Interface The Aptos CCTP V1 source code is [available on GitHub](https://github.com/circlefin/aptos-cctp/). The interface below serves as a reference for permissionless messaging functions exposed by the programs. ### TokenMessengerMinter #### [deposit\_for\_burn](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L123) Burns passed in `FungibleAsset` from sender to be minted on the destination domain. Minted tokens will be transferred to `mint_recipient` on the destination chain. The `mint_recipient` can be an account address or a store address. The `deposit_for_burn` interface and functionality is very similar to the EVM implementation. The asset parameter is the key difference due to how passing tokens around on Aptos works. The asset parameter is Aptos Fungible Asset type, defining information of the token to deposit and burn. Nonce reserved for the message is returned, but it is not required to do anything with this return value, they are returned for convenience. **Parameters** | Field | Type | Description | | :------------------ | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer executing the transaction | | asset | `FungibleAsset` | Asset to be burned. | | destination\_domain | `u32` | Destination domain identifier. | | mint\_recipient | `address` | Address of mint recipient on destination domain. Can be an account address or [store address](https://aptos.dev/en/build/smart-contracts/fungible-asset#managing-stores-advanced). *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* | #### [deposit\_for\_burn\_with\_caller](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L147) The same as deposit\_for\_burn, but with an additional parameter: `destination_caller`. This parameter specifies which address has permission to call `receiveMessage` on the destination domain for the message. **Parameters** | Field | Type | Description | | :------------------ | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer executing the transaction | | asset | `FungibleAsset` | Asset to be burned. | | destination\_domain | `u32` | Destination domain identifier. | | mint\_recipient | `address` | Address of mint recipient on destination domain. Can be an account address or [store address](https://aptos.dev/en/build/smart-contracts/fungible-asset#managing-stores-advanced). *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* | | destination\_caller | `address` | Address of caller on destination chain. | **Destination Caller Notes** If the `destination_caller` does not represent a valid address, then it will not be possible to broadcast the message on the destination domain. This is an advanced feature, and the standard `deposit_for_burn` should be preferred for use cases where a specific destination caller is not required. *Note: If destination is a non-Move chain,* `destination_caller` *address should be converted to hex and passed in using the @0x123 address format.* #### [replace\_deposit\_for\_burn\_with](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L171) Replace a BurnMessage to change the mint recipient and/or destination caller. Allows the sender of a previous BurnMessage (created by `deposit_for_burn` or `deposit_for_burn_with_caller`) to send a new BurnMessage to replace the original. **Remarks:** * Only the sender of the original `deposit_for_burn` transaction has access to call `replace_deposit_for_burn` * The new `BurnMessage` will reuse the amount and burn token of the original, without requiring a new `FA` deposit. * The resulting mint will supersede the original mint, as long as the original mint has not confirmed yet onchain. * A valid attestation is required before calling this function. * This is useful in situations where the user specified an incorrect address and has no way to safely mint the previously burned USDC. **Parameters** | Field | Type | Description | | :----------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer executing the transaction | | original\_message | `vector` | Original message bytes (to replace). | | original\_attestation | `vector` | Original attestation | | new\_destination\_caller | `Option
` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. | | new\_mint\_recipient | `Option
` | The new mint recipient, which may be the same as the original mint recipient, or different. | #### [handle\_receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#241) Handles an incoming message that has already been verified by `message_transmitter`, and mints USDC to the recipient for valid messages. This function can only be called with a mutable reference to a `Receipt` object, which can only be created via a call with a valid message to the `message_transmitter::receive_message` function. In this function `MessageTransmitter::complete_receive_message` is called with the `Receipt` to emit the `MessageReceived` event to complete the message and destroy Receipt. This should be called in a single transaction after calling `message_transmitter::receive_message`. EOAs can execute these functions in a single [Move script](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/scripts/handle_receive_message.move#L5). See the Examples section for the entire flow of receiving a message. **Parameters** | Field | Type | Description | | :------ | :-------- | :--------------------------------------------------------------- | | receipt | `Receipt` | Receipt struct returned by `message_transmitter:receive_message` | ### MessageTransmitter #### [receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L287) Receives a message emitted from a source chain. Messages with a given `nonce` can only be received once for a (sourceDomain, destinationDomain) pair. This function returns a `Receipt` struct ([Hot Potato](https://medium.com/@borispovod/move-hot-potato-pattern-bbc48a48d93c)) after validating the attestation and marking the nonce as used. In order to destroy the `Receipt` and complete the message, in a single transaction, `handle_receive_message()` must be called with the `Receipt` in the receiver package. EOAs can execute these functions in a single [Move script](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/scripts/handle_receive_message.move#L5). Returns a `Receipt` object that must be handled by the intended receiving package, and completed via a `complete_receive_message` call before the transaction ends. See the [Examples](/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base) section for more information on receiving USDC transfers. **Parameters** | Field | Type | Description | | :------------- | :----------- | :------------------------------- | | caller | `Signer` | Signer executing the transaction | | message\_bytes | `vector` | Message bytes | | attestation | `vector` | Attestation | #### [complete\_receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L330) Completes the message by emitting a `MessageReceived` event for a receipt and destroying the receipt. **Parameters** | Field | Type | Description | | :------ | :-------- | :----------------------------------------- | | caller | `Signer` | Signer for the `receipt.recipient` address | | receipt | `Receipt` | Receipt struct to be destroyed | #### [send\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L169) Sends a message to the destination domain and recipient. Nonce reserved for the message is returned, but it is not required to do anything with this struct, it is returned for convenience. **Remarks:** * For USDC transfers, this function is called directly by the `TokenMessengerMinter` package in `deposit_for_burn()`. **Parameters** | Field | Type | Description | | :------------------ | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer for executing the transaction | | destination\_domain | `u32` | Destination domain identifier. | | recipient | `address` | Recipient address. *Note: If destination is a non-Move chain,* `recipient` *address should be converted to hex and passed in using the @0x123 address format.* | | message\_body | `vector` | Message to be sent to destination chain | #### [send\_message\_with\_caller](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L198) This is the same as send\_message, except the `receive_message` call on the destination domain must be called by `destination_caller`. **Parameters** | Field | Type | Description | | :------------------ | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer for executing the transaction | | destination\_domain | `u32` | Destination domain identifier. | | recipient | `address` | Recipient address. *Note: If destination is a non-Move chain,* `recipient` *address should be converted to hex and passed in using the @0x123 address format.* | | destination\_caller | `address` | Address of caller on destination chain. | | message\_body | `vector` | Message to be sent to destination chain | **Destination Caller Notes** If the `destination_caller` does not represent a valid address, then it will not be possible to broadcast the message on the destination domain. This is an advanced feature, and the standard `deposit_for_burn` should be preferred for use cases where a specific destination caller is not required. *Note: If destination is a non-Move chain,* `destination_caller` *address should be converted to hex and passed in using the @0x123 address format.* #### [replace\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L229) Replace a message with a new message body and/or destination caller. The originalAttestation must be a valid attestation of originalMessage, produced by Circle's attestation service. **Remarks:** * Only the sender of the original deposit\_for\_burn transaction has access to call `replace_message` **Parameters** | Field | Type | Description | | :----------------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | caller | `Signer` | Signer executing the transaction | | original\_message | `vector` | Original message bytes (to replace). | | original\_attestation | `vector` | Original attestation | | new\_message\_body | `Option>` | New message body | | new\_destination\_caller | `Option
` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. | ## Additional Notes ### Using Move Scripts Pre-compiled scripts for executing `deposit_for_burn` and `handle_receive_message` are available in the [repo](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts) for testnet and mainnet. Alternatively, the scripts can be compiled from the source code. The documentation can be found on the official Aptos [website](https://aptos.dev/en/build/smart-contracts/scripts/compiling-scripts). ### Mint Recipient Addresses for Aptos as Source Chain Outgoing mint recipient addresses from Aptos are passed as Aptos address types and can be treated the same as a `bytes32` mint recipient parameter on EVM implementations. ### Mint Recipient Addresses for Aptos as Destination Chain Aptos mint recipient addresses from other chains should be treated the same as a hex `bytes32` parameter. # CCTP API Hosts and Endpoints V1 Source: https://developers.circle.com/cctp/v1/cctp-apis API hosts and endpoints for CCTP V1 **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. CCTP V1 provides a set of API hosts and endpoints to manage messages, attestations, and transaction details for your cross-chain USDC transfers. ## CCTP V1 API Service Hosts | Environment | URL | | :---------- | :------------------------------------ | | **Testnet** | `https://iris-api-sandbox.circle.com` | | **Mainnet** | `https://iris-api.circle.com` | ## CCTP V1 API Endpoints CCTP V1 endpoints allow you to fetch attestations for **Standard Transfer** burn events, verify public keys, and access transaction details. Below is an overview of the CCTP V1 public endpoints. Click on any endpoint for its API reference. | Endpoint | Description | Use Case | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | [`GET /v1/attestations/{messageHash}`](/api-reference/cctp/all/get-attestation) | Retrieves the signed attestation for a USDC burn event on the source chain. | Certifying the burn of USDC post hard finality. Used as a signal to mint USDC on the destination chain. | | [`GET /v1/publicKeys`](/api-reference/cctp/all/get-public-keys) | Fetches Circle's active public keys for verifying attestation signatures. | Validating the authenticity of Circle's signed attestations. | | [`GET /v1/messages/{sourceDomainId}/{transactionHash}`](/api-reference/cctp/all/get-messages) | Provides transaction details for burn events or associated messages. | Accessing detailed information about CCTP V1 transactions. | **API Service Rate Limit** The CCTP V1 API service rate limit is 35 requests per second. If you exceed 35 requests per second, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 response. # CCTP Supported Blockchains V1 Source: https://developers.circle.com/cctp/v1/cctp-supported-blockchains **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. CCTP V1 is available on the following blockchains where USDC is natively issued, providing **Standard Transfer** functionality. **Mainnet:** * Aptos * Arbitrum * Avalanche * Base * Ethereum * Noble * OP Mainnet * Polygon PoS * Solana * Sui * Unichain **Testnet:** * Aptos Testnet * Arbitrum Sepolia * Avalanche Fuji * Base Sepolia * Ethereum Sepolia * Noble Testnet * OP Sepolia * Polygon PoS Amoy * Solana Devnet * Sui Testnet * Unichain Sepolia # CCTP EVM Contracts and Interfaces V1 Source: https://developers.circle.com/cctp/v1/evm-smart-contracts CCTP V1 smart contracts for EVM-compatible blockchains **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Contract responsibilities * **TokenMessenger**: Entrypoint for cross-chain USDC transfer. Routes messages to burn USDC on a source chain, and mint USDC on a destination chain. * **MessageTransmitter**: Generic message passing. Sends all messages on the source chain, and receives all messages on the destination chain. * **TokenMinter**: Responsible for minting and burning USDC. Contains chain-specific settings used by burners and minters. * **Message**: Provides helper functions for cross-chain transfers, such as `bytes32ToAddress` and `addressToBytes32`, which are commonly used when bridging between EVM and non-EVM chains. These conversions are simple: prepend 12 zero bytes to an EVM address, or strip them to convert back. **Note:** If you're writing your own integration, it's more gas-efficient to [include this logic directly in your contract](https://github.com/circlefin/evm-cctp-contracts/blob/5f1901a9791b18204e8556bb53fb0dfcb05a832a/src/messages/Message.sol#L146) rather than calling an external one. Full contract source code is [available on GitHub](https://github.com/circlefin/evm-cctp-contracts). ## Mainnet contract addresses ### TokenMessenger: Mainnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0xBd3fa81B58Ba92a82136038B25aDec7066af3155`](https://etherscan.io/address/0xbd3fa81b58ba92a82136038b25adec7066af3155) | | **Avalanche** | 1 | [`0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982`](https://snowtrace.io/address/0x6b25532e1060ce10cc3b0a99e5683b91bfde6982) | | **OP Mainnet** | 2 | [`0x2B4069517957735bE00ceE0fadAE88a26365528f`](https://optimistic.etherscan.io/address/0x2B4069517957735bE00ceE0fadAE88a26365528f) | | **Arbitrum** | 3 | [`0x19330d10D9Cc8751218eaf51E8885D058642E08A`](https://arbiscan.io/address/0x19330d10D9Cc8751218eaf51E8885D058642E08A) | | **Base** | 6 | [`0x1682Ae6375C4E4A97e4B583BC394c861A46D8962`](https://basescan.org/address/0x1682Ae6375C4E4A97e4B583BC394c861A46D8962) | | **Polygon PoS** | 7 | [`0x9daF8c91AEFAE50b9c0E69629D3F6Ca40cA3B3FE`](https://polygonscan.com/address/0x9daf8c91aefae50b9c0e69629d3f6ca40ca3b3fe) | | **Unichain** | 10 | [`0x4e744b28E787c3aD0e810eD65A24461D4ac5a762`](https://uniscan.xyz/address/0x4e744b28E787c3aD0e810eD65A24461D4ac5a762) | ### MessageTransmitter: Mainnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0x0a992d191DEeC32aFe36203Ad87D7d289a738F81`](https://etherscan.io/address/0x0a992d191deec32afe36203ad87d7d289a738f81) | | **Avalanche** | 1 | [`0x8186359aF5F57FbB40c6b14A588d2A59C0C29880`](https://snowtrace.io/address/0x8186359af5f57fbb40c6b14a588d2a59c0c29880) | | **OP Mainnet** | 2 | [`0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8`](https://optimistic.etherscan.io/address/0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8) | | **Arbitrum** | 3 | [`0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca`](https://arbiscan.io/address/0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca) | | **Base** | 6 | [`0xAD09780d193884d503182aD4588450C416D6F9D4`](https://basescan.org/address/0xAD09780d193884d503182aD4588450C416D6F9D4) | | **Polygon PoS** | 7 | [`0xF3be9355363857F3e001be68856A2f96b4C39Ba9`](https://polygonscan.com/address/0xF3be9355363857F3e001be68856A2f96b4C39Ba9) | | **Unichain** | 10 | [`0x353bE9E2E38AB1D19104534e4edC21c643Df86f4`](https://uniscan.xyz/address/0x353bE9E2E38AB1D19104534e4edC21c643Df86f4) | ### TokenMinter: Mainnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0xc4922d64a24675E16e1586e3e3Aa56C06fABe907`](https://etherscan.io/address/0xc4922d64a24675e16e1586e3e3aa56c06fabe907) | | **Avalanche** | 1 | [`0x420F5035fd5dC62a167E7e7f08B604335aE272b8`](https://snowtrace.io/address/0x420f5035fd5dc62a167e7e7f08b604335ae272b8) | | **OP Mainnet** | 2 | [`0x33E76C5C31cb928dc6FE6487AB3b2C0769B1A1e3`](https://optimistic.etherscan.io/address/0x33E76C5C31cb928dc6FE6487AB3b2C0769B1A1e3) | | **Arbitrum** | 3 | [`0xE7Ed1fa7f45D05C508232aa32649D89b73b8bA48`](https://arbiscan.io/address/0xE7Ed1fa7f45D05C508232aa32649D89b73b8bA48) | | **Base** | 6 | [`0xe45B133ddc64bE80252b0e9c75A8E74EF280eEd6`](https://basescan.org/address/0xe45B133ddc64bE80252b0e9c75A8E74EF280eEd6) | | **Polygon PoS** | 7 | [`0x10f7835F827D6Cf035115E10c50A853d7FB2D2EC`](https://polygonscan.com/address/0x10f7835f827d6cf035115e10c50a853d7fb2d2ec) | | **Unichain** | 10 | [`0x726bFEF3cBb3f8AF7d8CB141E78F86Ae43C34163`](https://uniscan.xyz/address/0x726bFEF3cBb3f8AF7d8CB141E78F86Ae43C34163) | ### Message: Mainnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Ethereum** | 0 | [`0xB2f38107A18f8599331677C14374Fd3A952fb2c8`](https://etherscan.io/address/0xb2f38107a18f8599331677c14374fd3a952fb2c8) | | **Avalanche** | 1 | [`0x21F337db7A718F23e061262470Af8c1Fd01232D1`](https://snowtrace.io/address/0x21f337db7a718f23e061262470af8c1fd01232d1) | | **OP Mainnet** | 2 | [`0xDB2831EaF163be1B564d437A97372deB0046C70D`](https://optimistic.etherscan.io/address/0xdb2831eaf163be1b564d437a97372deb0046c70d) | | **Arbitrum** | 3 | [`0xE189BDCFbceCEC917b937247666a44ED959D81e4`](https://arbiscan.io/address/0xe189bdcfbcecec917b937247666a44ed959d81e4) | | **Base** | 6 | [`0x827ae40E55C4355049ab91e441b6e269e4091441`](https://basescan.org/address/0x827ae40E55C4355049ab91e441b6e269e4091441) | | **Polygon PoS** | 7 | [`0x02d9fa3e7f870E5FAA7Ca6c112031E0ddC5E646C`](https://polygonscan.com/address/0x02d9fa3e7f870E5FAA7Ca6c112031E0ddC5E646C) | | **Unichain** | 10 | [`0x395b1be6E432033B676e3e36B2c2121a1f952622`](https://uniscan.xyz/address/0x395b1be6E432033B676e3e36B2c2121a1f952622) | ## Testnet contract addresses ### TokenMessenger: Testnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Ethereum Sepolia** | 0 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia.etherscan.io/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5) | | **Avalanche Fuji** | 1 | [`0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0`](https://testnet.snowtrace.io/address/0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0) | | **OP Sepolia** | 2 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia-optimism.etherscan.io/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) | | **Arbitrum Sepolia** | 3 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia.arbiscan.io/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) | | **Base Sepolia** | 6 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://base-sepolia.blockscout.com/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5) | | **Polygon PoS Amoy** | 7 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://amoy.polygonscan.com/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) | | **Unichain Sepolia** | 10 | [`0x8ed94B8dAd2Dc5453862ea5e316A8e71AAed9782`](https://unichain-sepolia.blockscout.com/address/0x8ed94B8dAd2Dc5453862ea5e316A8e71AAed9782) | ### MessageTransmitter: Testnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Ethereum Sepolia** | 0 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://sepolia.etherscan.io/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) | | **Avalanche Fuji** | 1 | [`0xa9fB1b3009DCb79E2fe346c16a604B8Fa8aE0a79`](https://testnet.snowtrace.io/address/0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79) | | **OP Sepolia** | 2 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://sepolia-optimism.etherscan.io/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) | | **Arbitrum Sepolia** | 3 | [`0xaCF1ceeF35caAc005e15888dDb8A3515C41B4872`](https://sepolia.arbiscan.io/address/0xacf1ceef35caac005e15888ddb8a3515c41b4872) | | **Base Sepolia** | 6 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://base-sepolia.blockscout.com/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) | | **Polygon PoS Amoy** | 7 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://amoy.polygonscan.com/address/0x7865fafc2db2093669d92c0f33aeef291086befd) | | **Unichain Sepolia** | 10 | [`0xbc498c326533d675cf571B90A2Ced265ACb7d086`](https://unichain-sepolia.blockscout.com/address/0xbc498c326533d675cf571B90A2Ced265ACb7d086) | ### TokenMinter: Testnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Ethereum Sepolia** | 0 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia.etherscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) | | **Avalanche Fuji** | 1 | [`0x4ED8867f9947A5fe140C9dC1c6f207F3489F501E`](https://testnet.snowtrace.io/address/0x4ed8867f9947a5fe140c9dc1c6f207f3489f501e) | | **OP Sepolia** | 2 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia-optimism.etherscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) | | **Arbitrum Sepolia** | 3 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia.arbiscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) | | **Base Sepolia** | 6 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://base-sepolia.blockscout.com/address/0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A) | | **Polygon PoS Amoy** | 7 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://amoy.polygonscan.com/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) | | **Unichain Sepolia** | 10 | [`0x7348358C94519Da790DB38638d8c23669d343Bc6`](https://unichain-sepolia.blockscout.com/address/0x7348358C94519Da790DB38638d8c23669d343Bc6) | ### Message: Testnet | Chain | [Domain](/cctp/v1/supported-domains) | Address | | -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | **Ethereum Sepolia** | 0 | [`0x80537e4e8bAb73D21096baa3a8c813b45CA0b7c9`](https://sepolia.etherscan.io/address/0x80537e4e8bAb73D21096baa3a8c813b45CA0b7c9) | | **Avalanche Fuji** | 1 | [`0xeAf1DB5E3eb86FEbD8080368a956622b62Dcb78f`](https://testnet.snowtrace.io/address/0xeaf1db5e3eb86febd8080368a956622b62dcb78f) | | **OP Sepolia** | 2 | [`0xffbeA106ce4A3CdAfcC82BAebeD78C81814e32Ed`](https://sepolia-optimism.etherscan.io/address/0xffbeA106ce4A3CdAfcC82BAebeD78C81814e32Ed) | | **Arbitrum Sepolia** | 3 | [`0x70fAB9868cd54E12C7d87196424d6E0ca21be534`](https://sepolia.arbiscan.io/address/0x70fAB9868cd54E12C7d87196424d6E0ca21be534) | | **Base Sepolia** | 6 | [`0x8E52a9e76148185536F0f0779749Cc895E5f70dC`](https://base-sepolia.blockscout.com/address/0x8E52a9e76148185536F0f0779749Cc895E5f70dC) | | **Polygon PoS Amoy** | 7 | [`0x8E52a9e76148185536F0f0779749Cc895E5f70dC`](https://amoy.polygonscan.com/address/0x8E52a9e76148185536F0f0779749Cc895E5f70dC) | | **Unichain Sepolia** | 10 | [`0x1Fae490d95dDcFFD70728AF5024C524ed303a2e3`](https://unichain-sepolia.blockscout.com/address/0x1Fae490d95dDcFFD70728AF5024C524ed303a2e3) | ## CCTP V1 Interface This section provides the **CCTP V1 Smart Contract Interface** exposed by **CCTP V1**, outlining the available functions, and their parameters. The interface below serves as a reference for permissionless messaging functions exposed by the **TokenMessenger** and **MessageTransmitter** functions. The full ABIs are [available on GitHub](https://github.com/circlefin/evm-cctp-contracts/tree/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/docs/abis/cctp). ### TokenMessenger #### depositForBurn Deposits and burns tokens from sender to be minted on destination domain. Minted tokens will be transferred to `mintRecipient`. **Parameters** | Field | Type | Description | | ------------------- | --------- | ------------------------------------------------------------ | | `amount` | `uint256` | Amount of tokens to deposit and burn | | `destinationDomain` | `uint32` | Destination domain identifier | | `mintRecipient` | `bytes32` | Address of mint recipient on destination domain | | `burnToken` | `address` | Address of contract to burn deposited tokens on local domain | #### depositForBurnWithCaller Same as `depositForBurn` but with an additional parameter, `destinationCaller`. This parameter specifies which address has permission to call `receiveMessage` on the destination domain for the message. **Parameters** | Field | Type | Description | | ------------------- | --------- | ------------------------------------------------------------ | | `amount` | `uint256` | Amount of tokens to deposit and burn | | `destinationDomain` | `uint32` | Destination domain identifier | | `mintRecipient` | `bytes32` | Address of mint recipient on destination domain | | `burnToken` | `address` | Address of contract to burn deposited tokens on local domain | | `destinationCaller` | `bytes32` | Address of caller on the destination domain | #### replaceDepositForBurn Replace a `BurnMessage` to change the mint recipient and/or destination caller. Allows the sender of a previous `BurnMessage` (created by `depositForBurn` or `depositForBurnWithCaller`) to send a new `BurnMessage` to replace the original. The new `BurnMessage` will reuse the amount and burn token of the original, without requiring a new deposit. This is useful in situations where the user specified an incorrect address and has no way to safely mint the previously burned USDC. The sender of the original `depositForBurn` has access to call `replaceDepositForBurn`. The resulting mint will supersede the original mint, as long as the original mint has not confirmed yet onchain. When using a third-party app/bridge that integrates with CCTP V1 to burn and mint USDC, it is the choice of the app/bridge if and when to replace messages on behalf of users. When sending USDC to smart contracts, be aware of the functionality that those contracts have and their respective trust model. **Parameters** | Field | Type | Description | | ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `originalMessage` | `bytes calldata` | Original message bytes (to replace) | | `originalAttestation` | `bytes calldata` | Original attestation bytes | | `newDestinationCaller` | `bytes32` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid | | `newMintRecipient` | `bytes32` | The new mint recipient, which may be the same as the original mint recipient, or different | ### MessageTransmitter #### receiveMessage Messages with a given nonce can only be broadcast successfully once for a pair of domains. The message body of a valid message is passed to the specified recipient for further processing. **Parameters** | Field | Type | Description | | ------------- | ---------------- | ----------------------------- | | `message` | `bytes calldata` | Message bytes | | `attestation` | `bytes calldata` | Signed attestation of message | #### sendMessage Sends a message to the destination domain and recipient. Emits a `MessageSent` event which will be attested by Circle's attestation service (Iris). **Parameters** | Field | Type | Description | | ------------------- | ---------------- | ---------------------------------------------------- | | `destinationDomain` | `uint32` | Destination domain identifier | | `recipient` | `bytes32` | Address to handle message body on destination domain | | `messageBody` | `bytes calldata` | App-specific message to be handled by recipient | #### sendMessageWithCaller Same as `sendMessage` but with an additional parameter, `destinationCaller`. This parameter specifies which address has permission to call `receiveMessage` on the destination domain for the message. **Parameters** | Field | Type | Description | | ------------------- | ---------------- | -------------------------------------------------- | | `destinationDomain` | `uint32` | Destination domain identifier | | `recipient` | `bytes32` | Address of message recipient on destination domain | | `destinationCaller` | `bytes32` | Address of caller on the destination domain | | `messageBody` | `bytes calldata` | App-specific message to be handled by recipient | #### replaceMessage Replace a message with a new message body and/or destination caller. The `originalAttestation` must be a valid attestation of `originalMessage`, produced by Circle's attestation service (Iris). **Parameters** | Field | Type | Description | | ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `originalMessage` | `bytes calldata` | Original message to replace | | `originalAttestation` | `bytes calldata` | Attestation of `originalMessage` | | `newMessageBody` | `bytes calldata` | New message body of replaced message | | `newDestinationCaller` | `bytes32` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller (bytes32(0), indicating that any destination caller is valid) | # CCTP Message Passing V1 Source: https://developers.circle.com/cctp/v1/generic-message-passing CCTP V1 architecture on EVM and non-EVM domains **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. Cross-Chain Transfer Protocol V1 uses generalized message passing to facilitate the native burning and minting of USDC across supported blockchains, also known as [domains](/cctp/v1/supported-domains). Message passing is a three-step process: 1. An onchain component on the source domain emits a message. 2. Circle's offchain attestation service signs the message. 3. The onchain component at the destination domain receives the message, and forwards the message body to the specified recipient. ## Architecture Onchain components on all domains have the same purpose, but implementation differs between EVM-compatible and non-EVM domains. ### CCTP V1 on EVM Domains The relationship between CCTP V1's onchain components and Circle's offchain Attestation Service is illustrated below for a burn-and-mint of USDC between EVM-compatible domains: On EVM domains, the onchain component for cross-chain burning and minting is called **TokenMessenger**, which is built on top of **MessageTransmitter**, an onchain component for generalized message passing. In the diagram above, a token depositor calls the [TokenMessenger#depositForBurn](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/TokenMessenger.sol#L169) function to deposit a native token (such as USDC), which delegates to the **TokenMinter** contract to burn the token. The **TokenMessenger** contract then sends a message via the [MessageTransmitter#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L108) function. After [sufficient block confirmations](/cctp/v1/required-block-confirmations), Circle's offchain attestation service, Iris, signs the message. An API consumer queries this attestation and submits it onchain to the destination domain's [MessageTransmitter#receiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L250) function. For more details, see [Quickstart: Cross-chain USDC transfer](/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche). To send an arbitrary message, directly call [MessageTransmitter#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L108). Note that the message recipient must implement [IMessageHandler#handleReceiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/interfaces/IMessageHandler.sol#L31C14-L31C34). **Note:** In CCTP V1, it is not possible to perform a burn-and-mint operation for USDC and include arbitrary data in the same message. You must include arbitrary data in a separate message. In later CCTP versions, you can burn-and-mint while including data in the same message via Hooks. ### CCTP V1 on Non-EVM Domains #### Noble Noble is a Cosmos application-specific blockchain (or "appchain") that provides native asset issuance for the Cosmos ecosystem. USDC is natively issued on Noble and can be transferred via the Inter-Blockchain Communication (IBC) protocol to other supported appchains in Cosmos, or via CCTP V1 to any supported domain (for example, Ethereum). Note that there are key differences between Cosmos appchains like Noble and EVM-compatible blockchains. Unlike on EVM domains where CCTP V1 is a set of smart contracts, CCTP V1 on Noble is a Cosmos SDK module, which is deployed by Noble governance and built into the Noble blockchain. Cosmos appchains can use IBC to build composable flows with CCTP V1 on Noble. Refer to the [Noble documentation](/cctp/v1/noble-cosmos-module) for more details. #### Solana Solana is a layer-1 blockchain where USDC is natively issued as an SPL-token. CCTP V1 is deployed to Solana as two Anchor programs: **MessageTransmitter** and **TokenMessengerMinter**. Developers can compose programs on top of CCTP V1 programs through CPI's (Cross-Program Invocations). Arbitrary messages can be sent directly by calling `MessageTransmitter#send_message` just as described in the EVM section above. Refer to the [Solana documentation](/cctp/v1/solana-programs) for more details. #### Sui Sui is another layer-1 blockchain where USDC is natively issued as a [`Coin` implementation](https://docs.sui.io/guides/developer/coin). CCTP V1 is deployed to Sui as two programs: **MessageTransmitter** and **TokenMessengerMinter**. Arbitrary messages can be sent by directly calling `message_transmitter::send_message` similar to the EVM section above. Refer to the [Sui documentation](/cctp/v1/sui-packages) for more details. #### Aptos Aptos is another layer-1 blockchain where USDC is natively issued as a [`FA` implementation](https://aptos.dev/en/build/smart-contracts/fungible-asset). CCTP V1 is deployed to Aptos as two programs: **MessageTransmitter** and **TokenMessengerMinter**. Arbitrary messages can be sent by directly calling `message_transmitter::send_message` similar to the EVM section above. Refer to the [Aptos documentation](/cctp/v1/aptos-packages) for more details. # CCTP Limits V1 Source: https://developers.circle.com/cctp/v1/limits Limits for CCTP V1 burning and minting **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Minter Allowance The USDC smart contract (or module) on each blockchain specifies a limit for how much USDC can be minted before the limit needs to be increased by the master minter, Circle. This limit is called the "minter allowance" and it is individually set for each authorized minter, such as CCTP V1. Minter allowance is decremented each time the authorized minter mints, by the amount of USDC that is minted. A transaction attempting to mint in excess of the minter allowance will fail, but may succeed on a subsequent retry after the minter allowance is reset. Minter allowance can be queried from the USDC contract on EVM-compatible chains using the public [minterAllowance](https://github.com/centrehq/centre-tokens/blob/0d3cab14ebd133a83fc834dbd48d0468bdf0b391/contracts/v1/FiatTokenV1.sol#L153) function. For CCTP V1 on Noble, minter allowance can be queried via the [fiattokenfactory module minters API](https://github.com/circlefin/noble-fiattokenfactory/blob/33b30a6cf87eba20874df84fa93dd100f71ed512/proto/fiattokenfactory/query.proto#L49-L52). ## Per-Message Burn Limit CCTP V1 defines per-message burn limits. This value is configurable by Circle. This limit prevents the situation where a user burns an amount of USDC on a source chain that could never be minted on a destination chain without increasing minter allowance thresholds. Per-message burn limits can be queried on the TokenMinter contract on EVM-compatible chains, using the public [burnLimitsPerMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/roles/TokenController.sol#L69) mapping. For CCTP V1 on Noble, the per-message burn limit can be queried via the [cctp module per\_message\_burn\_limits API](https://github.com/circlefin/noble-fiattokenfactory/blob/master/proto/fiattokenfactory/query.proto#L49-L52). # CCTP Message Format V1 Source: https://developers.circle.com/cctp/v1/message-format Format arbitrary and application-specific messages using CCTP V1 **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## CCTP V1 Message Header The top-level message header format is standard for all messages passing through CCTP V1. | Field | Offset | Solidity Type | Length (bytes) | Description | | ------------------- | ------ | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- | | `version` | 0 | uint32 | 4 | Version identifier - use 0 for CCTP V1 | | `sourceDomain` | 4 | uint32 | 4 | Source domain ID | | `destinationDomain` | 8 | uint32 | 4 | Destination domain ID | | `nonce` | 12 | uint64 | 8 | Unique message nonce (see [Sequential Nonces](#sequential-nonces)) | | `sender` | 20 | bytes32 | 32 | Address of MessageTransmitter caller on source domain | | `recipient` | 52 | bytes32 | 32 | Address to handle message body on destination domain | | `destinationCaller` | 84 | bytes32 | 32 | Address permitted to call MessageTransmitter on destination domain, or bytes32(0) if message can be received by any address | | `messageBody` | 116 | bytes | dynamic | Application-specific message to be handled by recipient | ### Sequential Nonces A message nonce is a unique identifier for a message that can only be used once on the destination domain. In CCTP V1, message nonces are implemented using **Sequential Nonces**, where the next available nonce on a source domain is an integer. On the destination domain, messages can be received in any order, and used nonces are stored as a hash of the source domain and nonce integer value. **Why we use `bytes32` type for addresses** CCTP V1 is built to support EVM chains, which use 20 byte addresses, and non-EVM chains, many of which use 32 byte addresses. We provide a [Message.sol library](https://github.com/circlefin/evm-cctp-contracts/blob/40111601620071988e94e39274c8f48d6f406d6d/src/messages/Message.sol#L145-L157) as a reference implementation for converting between address and `bytes32` in Solidity. ## CCTP V1 Message Body The message format includes a dynamically sized `messageBody` field, used for application-specific messages. For example, TokenMessenger defines a [BurnMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/BurnMessage.sol) with data related to cross-chain transfers. | Field | Offset | Solidity Type | Length (bytes) | Description | | --------------- | ------ | ------------- | -------------- | -------------------------------------------------------------------------------------- | | `version` | 0 | uint32 | 4 | Version identifier (0, for CCTP V1) | | `burnToken` | 4 | bytes32 | 32 | Address of burned token on source domain | | `mintRecipient` | 36 | bytes32 | 32 | Address to receive minted tokens on destination domain | | `amount` | 68 | uint256 | 32 | Amount of burned tokens | | `messageSender` | 100 | bytes32 | 32 | Address of caller of `depositForBurn` (or `depositForBurnWithCaller`) on source domain | # CCTP Noble Cosmos Module V1 Source: https://developers.circle.com/cctp/v1/noble-cosmos-module Cosmos SDK Module for CCTP V1 support on Noble **This is CCTP (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Overview Noble is a Cosmos application-specific blockchain (or "appchain") that provides native asset issuance for the Cosmos ecosystem. USDC is natively issued on Noble and can be transferred via the Inter-Blockchain Communication Protocol (IBC) to other supported appchains in Cosmos, or via CCTP V1 to any supported domain (for example, Ethereum). Note that there are key differences between Cosmos appchains like Noble and EVM-compatible blockchains. Unlike on EVM chains where CCTP V1 is a set of smart contracts, CCTP V1 on Noble is a Cosmos SDK module, which is deployed by Noble governance and built into the Noble blockchain. Cosmos appchains can use IBC to build composable flows with CCTP V1 on Noble. ## Testnet and Mainnet Module Address | Chain | [Domain](/cctp/v1/supported-domains) | Address | | :---- | :----------------------------------- | :------------------------------------------- | | Noble | 4 | noble12l2w4ugfz4m6dd73yysz477jszqnfughxvkss5 | CCTP V1 on Noble source code is [available on GitHub](https://github.com/circlefin/noble-cctp). The full message spec is defined at [noble-cctp/x/cctp/spec/02\_messages.md](https://github.com/circlefin/noble-cctp/blob/dc81b3e0d566d195c869a213519fcecd38b020a5/x/cctp/spec/02_messages.md). The interface below serves as a reference for permissionless messaging functions exposed by the module. ## Module Interface ### depositForBurn **Message**: `MsgDepositForBurn` Broadcast a transaction that deposits for burn to a provided domain. **Arguments**: * `Amount` - The burn amount * `DestinationDomain` - Domain of destination chain * `MintRecipient` - address receiving minted tokens on destination chain as a 32 length byte array * `BurnToken` - The burn token address on source domain ### depositForBurnWithCaller **Message**:`MsgDepositForBurnWithCaller` Broadcast a transaction that deposits for burn with caller to a provided domain. This message wraps `MsgDepositForBurn`. It adds one extra argument, `destinationCaller`. **Arguments**: * `Amount` - The burn amount * `DestinationDomain` - Domain of destination chain * `MintRecipient` - address receiving minted tokens on destination chain as a 32 length byte array * `BurnToken` - The burn token address on source domain * `DestinationCaller` - authorized caller as 32 length byte array of receiveMessage() on destination domain ### replaceDepositForBurn **Message**: `MsgReplaceDepositForBurn` Broadcast a transaction that replaces a deposit for burn message. Replace the mint recipient and/or\ destination caller. Allows the sender of a previous BurnMessage (created by depositForBurn or depositForBurnWithCaller)\ to send a new BurnMessage to replace the original. The new BurnMessage will reuse the amount and\ burn token of the original without requiring a new deposit. **Arguments**: * `OriginalMessage`- original message bytes to replace * `OriginalAttestation`- attestation bytes of `OriginalMessage` * `NewDestinationCaller` - the new destination caller, which may be the\ same as the original destination caller, a new destination caller, or an empty\ destination caller, indicating that any destination caller is valid. * `NewMintRecipient` - the new mint recipient. May be the same as the\ original mint recipient, or different. ### receiveMessage **Message**: `MsgReceiveMessage` Broadcast a transaction that receives a provided message from another domain. After validation, it performs a mint. **Arguments**: * `message` [Message Format](/cctp/v1/message-format) * `attestation` - Concatenated 65-byte signature(s) of `message`, in increasing order\ of the attester address recovered from signatures. ### sendMessage **Message**:`MsgSendMessage` Broadcast a transaction that sends a message to a provided domain. **Arguments**: * `DestinationDomain` - Domain of destination chain * `Recipient` - Address of message recipient on destination chain * `MessageBody` - Raw bytes content of message ### sendMessageWithCaller **Message**:`MsgSendMessageWithCaller` Broadcast a transaction that sends a message with a caller to a provided domain. Specifying a Destination caller requires that only the specified caller can call `receiveMessage()` on destination domain. This message wraps `SendMessage` It adds one extra argument, `DestinationCaller`. **Arguments**: * `DestinationDomain` - Domain of destination chain * `Recipient` - Address of message recipient on destination chain * `MessageBody` - Raw bytes content of message * `DestinationCaller` - caller on the destination domain, as 32 length byte array ### replaceMessage **Message**: `MsgReplaceMessage` Broadcast a transaction that replaces a provided message. Replace the message body and/or destination caller. **Arguments**: * `OriginalMessage` - original message bytes to replace * `OriginalAttestation` - attestation bytes of `OriginalMessage` * `NewMessageBody` - new message body of replaced message * `NewDestinationCaller` - the new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. # CCTP Block Confirmations V1 Source: https://developers.circle.com/cctp/v1/required-block-confirmations Block confirmation requirements for attestations by chain **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. Before signing an attestation for a source chain event, Circle waits for a specified number of onchain block confirmations to achieve hard finality. The table below shows the average time required for an attestation to become available after a message is emitted onchain. **Note:** These values are subject to change. ## CCTP V1 Attestation Times | Source Chain | Number of Blocks | Average Time | | --------------- | ----------------- | -------------------- | | **Ethereum** | \~65\* | \~13 to 19 minutes\* | | **Avalanche** | 1 | \~8 seconds | | **OP Mainnet** | \~65 ETH blocks\* | \~13 to 19 minutes\* | | **Arbitrum** | \~65 ETH blocks\* | \~13 to 19 minutes\* | | **Noble** | 1 | \~20 seconds | | **Base** | \~65 ETH blocks\* | \~13 to 19 minutes\* | | **Polygon PoS** | \~33 | \~75 to 120 seconds | | **Solana** | 32 | \~25 seconds | | **Sui** | 1 | \~8 seconds | | **Aptos** | 1 | \~8 seconds | | **Unichain** | \~65 ETH blocks\* | \~13 to 19 minutes\* | **Block confirmations for L2s to Ethereum** Layer 2 (L2) blockchains publish transaction data in batches to Ethereum L1, and the frequency of these posts varies by chain. Some submit batches every few minutes, while others are less frequent. After a batch is posted, Circle waits for the Ethereum L1 block containing the batch to finalize, which typically happens after \~65 blocks, or 13 to 19 minutes, before issuing an attestation. # CCTP Solana Programs and Interfaces V1 Source: https://developers.circle.com/cctp/v1/solana-programs Programs for CCTP V1 support on the Solana blockchain **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Overview Solana CCTP V1 programs are written in Rust and leverage the Anchor framework. The Solana CCTP V1 protocol implementation is split into two programs: `MessageTransmitter` and `TokenMessengerMinter`. `TokenMessengerMinter` encapsulates the functionality of both `TokenMessenger` and `TokenMinter` contracts on EVM chains. To ensure alignment with EVM contracts' logic and state, and to facilitate future upgrades and maintenance, the code and state of Solana programs reflect the EVM counterparts as closely as possible. ### Mainnet Program Addresses | Program | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | | MessageTransmitter | 5 | [`CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd`](https://solscan.io/account/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd) | | TokenMessengerMinter | 5 | [`CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3`](https://solscan.io/account/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3) | ### Devnet Program Addresses | Program | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | | MessageTransmitter | 5 | [`CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd`](https://solscan.io/account/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd?cluster=devnet) | | TokenMessengerMinter | 5 | [`CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3`](https://solscan.io/account/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3?cluster=devnet) | The Solana CCTP V1 source code is [available on GitHub](https://github.com/circlefin/solana-cctp-contracts/). The interface below serves as a reference for permissionless messaging functions exposed by the programs. ## CCTP V1 Interface The interface below serves as a reference for permissionless messaging functions exposed by the `TokenMessengerMinter` and `MessageTransmitter` programs. The full IDLs can be found onchain using a block explorer. [MessageTransmitter IDL](https://explorer.solana.com/address/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3/anchor-program) and [TokenMessengerMinter IDL](https://explorer.solana.com/address/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd/anchor-program). *Please see the instruction rust files or quick-start for PDA information.* ### TokenMessengerMinter ### [depositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/deposit_for_burn.rs) Deposits and burns tokens from sender to be minted on destination domain. Minted tokens will be transferred to `mintRecipient`. **Parameters** | Field | Type | Description | | :---------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | amount | u64 | Amount of tokens to deposit and burn. | | destinationDomain | u32 | Destination domain identifier. | | mintRecipient | Pubkey | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* | **MessageSent event storage** To ensure persistent and reliable message storage, MessageSent events are stored in accounts. MessageSent event accounts are generated client-side, passed into the instruction call, and assigned to have the `MessageTransmitter` program as the owner. Please see the [Quickstart Guide](/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche) for how to generate this account and pass it to the instruction call. For `depositForBurn` messages, this costs `~0.00295104 SOL` in rent. *This rent is paid by the [`event_rent_payer`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message.rs#L16C9-L16C25) account which can be the user or subsidized by a calling program or integrator.* Once an attestation is available and the message has been received on the destination chain, the event account can be closed and have the SOL reclaimed to the `event_rent_payer` account. This is done by calling the [`reclaim_event_account`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/reclaim_event_account.rs) instruction. This can only be called by the `event_rent_payer` account from when the message was sent. Details on the message format can be found on the [Message Format page](/cctp/v1/message-format). ### [depositForBurnWithCaller](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/deposit_for_burn_with_caller.rs) The same as `depositForBurn` but with an additional parameter, `destinationCaller`. This parameter specifies which address has permission to call `receiveMessage` on the destination domain for the message. **Parameters** | Field | Type | Description | | :---------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | amount | u64 | Amount of tokens to deposit and burn. | | destinationDomain | u32 | Destination domain identifier. | | mintRecipient | Pubkey | Public Key of mint recipient on destination domain. *Address should be converted to base58.* See \[Mint Recipient for Solana as Source Chain Transfers]\(Mint Recipient for Solana as Source Chain Transfers) | | destinationCaller | Pubkey | Public Key of caller on destination domain. *Address should be converted to base58.* | ### [replaceDepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/replace_deposit_for_burn.rs) Replace a `BurnMessage` to change the mint recipient and/or destination caller. Allows the sender of a previous `BurnMessage` (created by `depositForBurn` or `depositForBurnWithCaller`) to send a new `BurnMessage` to replace the original. The new `BurnMessage` will reuse the amount and burn token of the original, without requiring a new deposit. This is useful in situations where the user specified an incorrect address and has no way to safely mint the previously burned USDC. **Note on replaceDepositForBurn** Only the owner account of the original depositForBurn has access to call replaceDepositForBurn. The resulting mint will supersede the original mint, as long as the original mint has not confirmed yet onchain. When using a third-party app/bridge that integrates with CCTP V1 to burn and mint USDC, it is the choice of the app/bridge if and when to replace messages on behalf of users. When sending USDC to smart contracts, be aware of the functionality that those contracts have and their respective trust model. **Parameters** | Field | Type | Description | | :------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | originalMessage | Vec\ | Original message bytes (to replace). | | originalAttestation | Vec\ | Original attestation bytes. | | newDestinationCaller | Pubkey | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. *Address should be converted to base58.* | | newMintRecipient | Pubkey | The new mint recipient, which may be the same as the original mint recipient, or different. *Address should be converted to base58.* | ### MessageTransmitter ### [receiveMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/receive_message.rs) Messages with a given nonce can only be broadcast successfully once for a pair of domains. The message body of a valid message is passed to the specified recipient for further processing. **Parameters** | Field | Type | Description | | :---------- | :------- | :----------------------------- | | message | Vec\ | Message bytes. | | attestation | Vec\ | Signed attestation of message. | **Remaining Accounts** If the `receiveMessage` instruction is being called with a deposit for burn message that will be received by the `TokenMessengerMinter`, additional `remainingAccounts` are required so they can be passed with the CPI to `TokenMessengerMinter#handleReceiveMessage`: | Account Name | PDA Seeds | PDA ProgramId | isSigner? | isWritable? | Description | | :------------------------------ | :---------------------------------------------------- | :------------------- | :-------- | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `token_messenger` | `["token_messenger"]` | tokenMessengerMinter | false | false | TokenMessenger Program Account | | `remote_token_messenger` | `["remote_token_messenger", sourceDomainId]` | tokenMessengerMinter | false | false | Remote token messenger account where the remote token messenger address is stored for the given source domain id | | `token_minter` | `["token_minter"]` | tokenMessengerMinter | false | true | TokenMinter Program Account | | `local_token` | `["local_token", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Local token account where the information for the local token (e.g. USDCSOL) being minted is stored | | `token_pair` | `["token_pair", sourceDomainId, sourceTokenInBase58]` | tokenMessengerMinter | false | false | Token pair account where the info for the local and remote tokens are stored. `sourceTokenInBase58` is the remote token that was burned converted into base58 format. | | `user_token_account` | N/A | N/A | false | true | User token account that will receive the minted tokens. This address **must** match the mintRecipient from the source chain depositForBurn call. | | `custody_token_account` | `["custody", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Custody account that holds the pre-minted USDCSOL that can be minted for CCTP V1 usage. | | `SPL.token_program_id` | N/A | N/A | false | false | The native SPL token program ID. | | `token_program_event_authority` | `["__event_authority"]` | tokenMessengerMinter | false | false | Event authority account for the TokenMessengerMinter program. Needed to emit Anchor CPI events. | | `program` | N/A | N/A | false | false | Program id for the TokenMessengerMinter program. | ### [sendMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message.rs) Sends a message to the destination domain and recipient. Emits a `MessageSent` event which will be attested by Circle's attestation service. **Parameters** | Field | Type | Description | | :---------------- | :------- | :------------------------------------------------------- | | destinationDomain | u32 | Destination domain identifier. | | recipient | Pubkey | Address to handle message body on destination domain. | | messageBody | Vec\ | Application-specific message to be handled by recipient. | ### [sendMessageWithCaller](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message_with_caller.rs) Same as `sendMessage` but with an additional parameter, `destinationCaller`. This parameter specifies which address has permission to call `receiveMessage` on the destination domain for the message. **Parameters** | Field | Type | Description | | :---------------- | :------- | :------------------------------------------------------------------------------------ | | destinationDomain | u32 | Destination domain identifier. | | recipient | Pubkey | Address of message recipient on destination domain. | | destinationCaller | Pubkey | Address of caller on the destination domain. *Address should be converted to base58.* | | messageBody | Vec\ | Application-specific message to be handled by recipient. | ### [replaceMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/replace_message.rs) Replace a message with a new message body and/or destination caller. The `originalAttestation` must be a valid attestation of `originalMessage`, produced by Circle's attestation service. **Parameters** | Field | Type | Description | | :------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | originalMessage | Vec\ | Original message to replace. | | originalAttestation | Vec\ | Attestation of originalMessage. | | newMessageBody | Vec\ | New message body of replaced message. | | newDestinationCaller | Pubkey | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller (bytes32(0) or `PublicKey.default`, indicating that any destination caller is valid). *Address should be converted to base58.* | ### [reclaimEventAccount](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/reclaim_event_account.rs) Closes the given event account and reclaims the paid rent in SOL back to the `event_rent_payer` account. This instruction can only be called by the `event_rent_payer` account that paid the rent when the message was sent. **Parameters** | Field | Type | Description | | :---------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | attestation | Vec\ | Valid attestation for the message stored in the account. This is required to ensure the attestation service has processed and stored the message before it is deleted. | **Warning** Once this instruction is executed for a message account, the message can no longer be read onchain. We recommend not calling this instruction until the message has been received on the destination chain. If the message is lost before receiving the message, it can be fetched from the attestation service using the [messages endpoint](/api-reference/cctp/all/get-messages). ## Additional Notes ### Mint Recipient for Solana as Destination Chain Transfers When calling `depositForBurn` on a non-Solana chain with Solana as the destination, the `mintRecipient` should be a **hex encoded USDC token account address**. The token account\* must exist at the time `receiveMessage` is called on Solana\* or else this instruction will revert. An example of converting an address from Base58 to hex taken from the Solana quickstart tutorial in TypeScript can be seen below: ```typescript TypeScript theme={null} import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; import { hexlify } from "ethers"; const solanaAddressToHex = (solanaAddress: string): string => hexlify(bs58.decode(solanaAddress)); ``` ### Mint Recipient for Solana as Source Chain Transfers When specifying the `mintRecipient` for Solana `deposit_for_burn` instruction calls, the address must be given as the 32 byte version of the hex address in base58 format. An example taken from the Solana quickstart tutorial in TypeScript can be seen below: ```typescript TypeScript theme={null} import { getBytes } from "ethers"; import { PublicKey } from "@solana/web3.js"; const evmAddressToBytes32 = (address: string): string => `0x000000000000000000000000${address.replace("0x", "")}`; const evmAddressToBase58PublicKey = (addressHex: string): PublicKey => new PublicKey(getBytes(evmAddressToBytes32(addressHex))); ``` ### Program Events Program events like [DepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L35-L45) , [MintAndWithdraw](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52) , and [MessageReceived](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52) are emitted as Anchor CPI events. This means a self-CPI is made into the program with the serialized event as instruction data so it is persisted in the transaction and can be fetched later on as needed. More information can be seen in the [Anchor implementation PR](https://github.com/coral-xyz/anchor/pull/2438), and an example of reading CPI events can be seen in the [solana-cctp-contracts repository](https://github.com/circlefin/solana-cctp-contracts/blob/master/tests/utils.ts#L62-L111). [MessageSent](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/events.rs#L49-L55) events are different, as they are stored in accounts. Please see the [MessageSent Event Storage section](#depositforburn) for more info. # CCTP Sui Packages and Interfaces V1 Source: https://developers.circle.com/cctp/v1/sui-packages Packages for CCTP V1 support on the Sui blockchain **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. ## Overview The CCTP V1 Sui smart contract implementation is written in [Move](https://sui.io/move). The Sui CCTP V1 implementation is split into two packages: `MessageTransmitter` and `TokenMessengerMinter`. `TokenMessengerMinter` encapsulates the functionality of both `TokenMessenger` and `TokenMinter` contracts on EVM chains. To ensure alignment with EVM contracts logic and state, and to facilitate future upgrades and maintenance, the code and state of the Sui packages reflect the EVM counterparts as closely as possible. There are a few key differences with Sui packages from EVM and other CCTP V1 implementations: ### Receive Message Flow Since the Move language does not have interfaces, the `message_transmitter::receive_message()` function cannot call directly into the receiver package (e.g. `TokenMessenger` for USDC transfers). The workaround for this limitation is that callers of `receive_message()` must also atomically (in the same [Programmable Transaction Block (PTB)](https://docs.sui.io/concepts/transactions/prog-txn-blocks)) call into the receiver package's `handle_receive_message()` function with a `Receipt` struct, call `stamp_receipt()` with the `StampReceiptTicket` struct returned from `handle_receive_message()`, and then pass the `StampedReceipt` back into the `message_transmitter::complete_receive_message()` function to complete the message and destroy the `Receipt` object. This flow ensures atomicity and guarantees message receipt by the receiver packages. Please see the interface and examples below for more information on this flow. ### Interacting with TokenMessengerMinter from other Packages On Sui, when a package is upgraded, the new version is deployed with a new package ID. This means if another package is directly calling a version-gated function, when the package is upgraded, the dependent packages must also be upgraded. To address this, all CCTP V1 functions that are intended to be called from a dependent package follow a `Ticket` struct pattern. In this pattern, the dependent package can call non version-gated `create_ticket()` functions with the intended function parameters, including an `Auth` struct (used to uniquely identify the package), and receive back a `Ticket` struct. This struct can then be returned from the dependent package and used in a PTB to call the intended CCTP V1 function. This allows integrators to securely integrate with CCTP V1 functions from their packages, and only have to update PTBs when CCTP V1 packages are upgraded rather than having to upgrade their packages as well. For more information, see the functions below with the `_with_package_auth` suffix. ### Testnet #### Package IDs | Package | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :------------------------------------------------------------------- | | MessageTransmitter | 8 | `0x4931e06dce648b3931f890035bd196920770e913e43e45990b383f6486fdd0a5` | | TokenMessengerMinter | 8 | `0x31cc14d80c175ae39777c0238f20594c6d4869cfab199f40b69f3319956b8beb` | #### Shared Object IDs | Object | Object ID | | :------------------------ | :------------------------------------------------------------------- | | MessageTransmitterState | `0x98234bd0fa9ac12cc0a20a144a22e36d6a32f7e0a97baaeaf9c76cdc6d122d2e` | | TokenMessengerMinterState | `0x5252abd1137094ed1db3e0d75bc36abcd287aee4bc310f8e047727ef5682e7c2` | | USDC Treasury Object | `0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe` | Branch with testnet [Automated Address Management](https://docs.sui.io/concepts/sui-move-concepts/packages/automated-address-management): [github.com/circlefin/sui-cctp/tree/testnet](https://github.com/circlefin/sui-cctp/tree/testnet). ### Mainnet #### Package IDs | Package | [Domain](/cctp/v1/supported-domains) | Address | | :------------------- | :----------------------------------- | :------------------------------------------------------------------- | | MessageTransmitter | 8 | `0x08d87d37ba49e785dde270a83f8e979605b03dc552b5548f26fdf2f49bf7ed1b` | | TokenMessengerMinter | 8 | `0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e` | #### Shared Object IDs | Object | Object ID | | :------------------------ | :------------------------------------------------------------------- | | MessageTransmitterState | `0xf68268c3d9b1df3215f2439400c1c4ea08ac4ef4bb7d6f3ca6a2a239e17510af` | | TokenMessengerMinterState | `0x45993eecc0382f37419864992c12faee2238f5cfe22b98ad3bf455baf65c8a2f` | | USDC Treasury Object | `0x57d6725e7a8b49a7b2a612f6bd66ab5f39fc95332ca48be421c3229d514a6de7` | Branch with mainnet [Automated Address Management](https://docs.sui.io/concepts/sui-move-concepts/packages/automated-address-management): [github.com/circlefin/sui-cctp/tree/mainnet](https://github.com/circlefin/sui-cctp/tree/mainnet). ## Interface The Sui CCTP V1 source code is [available on GitHub](https://github.com/circlefin/sui-cctp/). The interface below serves as a reference for permissionless messaging functions exposed by the programs. ### TokenMessengerMinter #### [deposit\_for\_burn](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L106) Burns passed in tokens from sender to be minted on destination domain. Minted tokens will be transferred to `mint_recipient` on the destination chain. The `deposit_for_burn` interface and functionality is very similar to the EVM implementation. The coins parameter is the key difference due to how passing tokens around on Sui works. `message_transmitter_state`, `deny_list`, and `treasury` parameters are all shared objects. **Remarks:** * Intended to be called directly by EOA (rather than a dependent package). The initiating EOA will be the "owner" (e.g. message sender) of the message and have the ability to call `replace_deposit_for_burn()` to update the `mint_recipient` or `destination_caller`. If the calling EOA is not trusted by the mint recipient or destination caller, `deposit_for_burn_with_package_auth()` should be called instead with the integrating package owning the message. * The generic type T is the coin's one-time witness ([OTW](https://docs.sui.io/concepts/sui-move-concepts/one-time-witness)) type for the specific coin type to be burned. * `BurnMessage` and `Message` structs are returned, but it is not required to do anything with these structs; they are returned for convenience. **Parameters** | Field | Type | Description | | :-------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | coins | `Coin` | Coin of type T to be burned. Full amount in coins will be burned. | | destination\_domain | `u32` | Destination domain identifier. | | mint\_recipient | `address` | Address of mint recipient on destination domain. *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* | | state | `&State` | Shared State object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. | | deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` | | treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [deposit\_for\_burn\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L143) Same as `deposit_for_burn()`, but intended to be called with an `Auth` struct from a dependent package. The calling package will be the "owner" (e.g. message\_sender) of the message and have the ability to call `replace_deposit_for_burn_with_package_auth()` to update the mint recipient or destination caller. This would be similar to a wrapper contract on EVM chains calling into `TokenMessenger` and being the message sender. Direct callers (where EOAs are trusted and should be the owner) should use `deposit_for_burn()` instead. **Remarks:** * This function uses a `DepositForBurnTicket` struct for parameters so that the calling package can call `create_deposit_for_burn_ticket()` (not version-gated) from their package with parameters, and call `deposit_for_burn_with_package_auth()` (version-gated) from a PTB so packages don't have to be updated during CCTP V1 package upgrades. * `DepositForBurnTicket` also requires an `Auth` parameter. This is required to securely assign a sender address associated with the calling contract to the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated `Auth` struct. Calling contracts should be careful to not expose these structs to the public or else messages from their package could be replaced. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). * The returned structs - `BurnMessage` and `Message` both have the copy ability. There is also no guarantee of execution ordering, so your package could create 5 DepositForBurnTickets in one transaction and they could be executed in any order depending on the PTB. Integrating packages should account for both of these scenarios. **Parameters** | Field | Type | Description | | :-------------------------- | :------------------------------ | :----------------------------------------------------------------------------- | | deposit\_for\_burn\_ticket | `DepositForBurnTicket` | Struct containing parameters and authenticator struct. | | state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. | | deny\_list | `&DenyList` | DenyList shared object for the stablecoin token `T`. Constant address: `0x403` | | treasury | `&mut Treasury` | Treasury shared object for the stablecoin token `T`. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [deposit\_for\_burn\_with\_caller](https://github.com/circlefin/sui-cctp/blob/649ed8a06840271ddc1ad66bb215d51be8265c31/packages/token_messenger_minter/sources/deposit_for_burn.move#L177) Same as `deposit_for_burn` but with an additional parameter, `destination_caller`. This parameter specifies which address has permission to call `receive_message` on the destination domain for the message. **Remarks:** * Intended to be called directly by EOA (rather than a dependent package). The initiating EOA will be the "owner" (e.g. message sender) of the message and have the ability to call `replace_deposit_for_burn()` to update the `mint_recipient` or `destination_caller`. If the calling EOA is not trusted by the mint recipient or destination caller, `deposit_for_burn_with_caller_with_package_auth()` should be called instead with the integrating package owning the message. **Destination Caller Notes** If the `destination_caller` does not represent a valid address, then it will not be possible to broadcast the message on the destination domain. This is an advanced feature, and the standard `deposit_for_burn` should be preferred for use cases where a specific destination caller is not required. *Note: If destination is a non-Move chain,* `destination_caller` *address should be converted to hex and passed in using the @0x123 address format.* **Parameters** | Field | Type | Description | | :-------------------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | coins | `Coin` | Coin of type T to be burned. Full amount in coins will be burned. | | destination\_domain | `u32` | Destination domain identifier. | | mint\_recipient | `address` | Address of mint recipient on destination domain *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* | | destination\_caller | `address` | Address of caller on destination chain. | | state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. | | deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` | | treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [deposit\_for\_burn\_with\_caller\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L212) The same as `deposit_for_burn_with_caller()`, but intended to be called with an `Auth` struct from a dependent package. The calling package will be the "owner" (e.g. message\_sender) of the message and have the ability to call `replace_deposit_for_burn_with_package_auth()` to update the `mint_recipient` or `destination_caller`. This would be similar to a wrapper contract on EVM chains calling into `TokenMessenger` and being the message sender. Direct callers (where EOAs are trusted and should be the owner) should use `deposit_for_burn_with_caller()` instead. **Remarks:** * This function uses a `DepositForBurnWithCallerTicket` struct for parameters so that the calling package can call `create_deposit_for_burn_with_caller_ticket()` (not version-gated) from their package, and call `deposit_for_burn_with_caller_with_package_auth()` (version-gated) from a PTB so dependent packages don't have to be updated during upgrades. * `DepositForBurnWithCallerTicket` also requires an `Auth` parameter. This is required to securely assign a sender address associated with the calling contract to the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated struct. Calling contracts should be careful to not expose these structs to the public or else messages from their package could be replaced. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). **Destination Caller Notes** If the `destination_caller` does not represent a valid address, then it will not be possible to broadcast the message on the destination domain. This is an advanced feature, and the standard `deposit_for_burn` should be preferred for use cases where a specific destination caller is not required. *Note: If destination is a non-Move chain,* `destination_caller` *address should be converted to hex and passed in using the @0x123 address format.* **Parameters** | Field | Type | Description | | :--------------------------------------- | :---------------------------------------- | :--------------------------------------------------------------------------- | | deposit\_for\_burn\_with\_caller\_ticket | `DepositForBurnWithCallerTicket` | Struct containing parameters and authenticator struct. | | state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. | | deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` | | treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [replace\_deposit\_for\_burn](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L259) Replace a `BurnMessage` to change the mint recipient and/or destination caller. Allows the sender of a previous `BurnMessage` (created by `deposit_for_burn` or `deposit_for_burn_with_caller`) to send a new `BurnMessage` to replace the original. **Remarks:** * The new `BurnMessage` will reuse the amount and burn token of the original, without requiring a new `Coin` deposit. * The resulting mint will supersede the original mint, as long as the original mint has not confirmed yet onchain. * A valid attestation is required before calling this function. * This is useful in situations where the user specified an incorrect address and has no way to safely mint the previously burned USDC. **Parameters** | Field | Type | Description | | :-------------------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | original\_message | `vector` | Original message bytes (to replace). | | original\_attestation | `vector` | Original attestation bytes. | | new\_destination\_caller | `Option
` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. | | new\_mint\_recipient | `Option
` | The new mint recipient, which may be the same as the original mint recipient, or different. | | state | `&State` | Shared State object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [replace\_deposit\_for\_burn\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L282) Same as `replace_deposit_for_burn()`, but intended to be called when `deposit_for_burn_with_package_auth()` or `deposit_for_burn_with_caller_with_package_auth()` was called for the original message where the calling package is the message sender. **Remarks:** * This function uses a `ReplaceDepositForBurnTicket` struct for parameters so that the calling package can call `create_replace_deposit_for_burn_ticket()` (not version-gated) from their package with parameters, and call `deposit_for_burn_with_package_auth()` (version-gated) from a PTB so packages don't have to be updated during CCTP V1 package upgrades. **Parameters** | Field | Type | Description | | :---------------------------------- | :---------------------------------- | :---------------------------------------------------------- | | replace\_deposit\_for\_burn\_ticket | `ReplaceDepositForBurnTicket` | Struct containing parameters and authenticator struct. | | state | `&State` | Shared State object for the `TokenMessengerMinter` package. | | message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [handle\_receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/handle_receive_message.move#L131) Handles an incoming message from `MessageTransmitter`, and mints USDC to the recipient for valid messages. This function can only be called with a mutable reference to a `Receipt` object, which can only be created via a call with a valid message to the `message_transmitter::receive_message()` function. `state`, `mt_state`, `deny_list`, and `treasury` parameters are all shared objects. **Remarks:** * Returns a `StampReceiptTicketWithBurnMessage` struct that can be deconstructed in a dependent package (or in a PTB) via `deconstruct_stamp_receipt_ticket_with_burn_message()`. This struct is returned so that dependent packages can associate the `BurnMessage` and `StampReceiptTicket` together from a PTB call and guarantee that `stamp_receipt()` was called. * This must be called in a single PTB after calling `receive_message()` and before calling `complete_receive_message()`. See the [Examples](/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum) page for the entire flow of receiving a message. **Parameters** | Field | Type | Description | | :--------- | :----------------- | :--------------------------------------------------------------------------- | | receipt | `Receipt` | Original message bytes (to replace). | | state | `&State` | Shared State object for the `TokenMessengerMinter` package. | | deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` | | treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. | | ctx | `&TxContext` | `TxContext` for the transaction. | ### MessageTransmitter #### [receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L113) Receives a message emitted from a source chain. Messages with a given nonce can only be received once for a (`sourceDomain`, `destinationDomain`) pair. **Remarks:** * This function returns a `Receipt` [Hot Potato](https://medium.com/@borispovod/move-hot-potato-pattern-bbc48a48d93c) struct after validating the attestation and marking the nonce as used. * In order to destroy the `Receipt` and complete the message, in a single PTB, `stamp_receipt()` must be called with the `Receipt` and an `Auth` struct (see [message\_transmitter\_authenticator](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L212) for an example of this), and then `complete_receive_message()` must be called with the `StampedReceipt` to emit the `MessageReceived` event and complete the message. * The receipt/stamp pattern is used to enforce atomicity and ensure the intended receiver contract is called. * Intended to be called directly from an EOA when a package `destination_caller` is not specified on the message. Please use `receive_message_with_package_auth()` if a package `destination_caller` is specified. **Parameters** | Field | Type | Description | | :---------- | :----------- | :---------------------------------------------------------- | | message | `vector` | Message bytes. | | attestation | `vector` | Signed attestation of message. | | state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [receive\_message\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L146) Same as `receive_message()`, except intended to be used by a dependent package when a package is specified as `destination_caller` (rather than an EOA). **Remarks:** * This function is version-gated and should be called from a PTB to prevent breaking changes when an upgrade occurs. * This function uses a `ReceiveMessageTicket` for parameters so that the calling package can call `create_receive_message_ticket()` (not version-gated) from their package with parameters, and call `receive_message_with_package_auth()` (version-gated) from a PTB so packages don't have to be upgraded during CCTP V1 package upgrades. * `ReceiveMessageTicket` also requires an `Auth` parameter. This is required whenever a package is assigned as a `destination_caller`. `destination_caller` address should be set to the `Auth` identifier returned from the `auth_caller_identifier()` function with the package's `Auth` struct. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated `Auth` struct. Calling contracts should be careful to not expose these structs to the public or else messages intended for their package could be received by others. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). **Parameters** | Field | Type | Description | | :----------------------- | :--------------------------- | :---------------------------------------------------------------------------------- | | receive\_message\_ticket | `ReceiveMessageTicket` | A `Ticket` struct containing the message, attestation, and an authenticator struct. | | state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [stamp\_receipt](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L164) Stamps a `Receipt` struct after verifying the intended package acknowledged the message (through the `Auth` struct) by returning a `StampedReceipt` struct that can be used to complete the message via `complete_receive_message()`. **Remarks:** * This function is version-gated and should be called from a PTB to prevent breaking changes in dependent packages when a CCTP V1 upgrade occurs. * `create_stamp_receipt_ticket()` is safe to be called directly from a package (not version-gated), and it's returned `Ticket` struct can be passed into `stamp_receipt()` in a PTB. **Auth Parameter Notes** This is required for the `MessageTransmitter` module to approve a `Receipt` prior to its deletion. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated `Auth` struct. Calling contracts should be careful to not expose these `Auth` structs to the public to avoid messages being wrongly stamped. An example implementation exists in the `token_messenger_minter::message_transmitter_authenticator` module. **Parameters** | Field | Type | Description | | :--------------------- | :------------------------- | :----------------------------------------------------------------------------------------------- | | stamp\_receipt\_ticket | `StampReceiptTicket` | `Ticket` struct created by `create_stamp_receipt_ticket()` with the `Receipt` and `Auth` struct. | | state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. | | ctx | `&TxContext` | `TxContext` for the transaction. | #### [complete\_receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L178) Completes the message by emitting a `MessageReceived` event for a stamped receipt and destroying the receipt. Cannot be called without a `StampedReceipt` (returned from `stamp_receipt()`). **Parameters** | Field | Type | Description | | :--------------- | :--------------- | :---------------------------------------------------------- | | stamped\_receipt | `StampedReceipt` | A stamped receipt returned from a `stamp_receipt()` call. | | state | `&State` | Shared State object for the `TokenMessengerMinter` package. | #### [send\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L66) Sends a message to the destination domain and recipient. The created `Message` struct is returned, but it is not required to do anything with this struct, it is returned for convenience. **Remarks:** * This function uses a `SendMessageTicket` for parameters so that the calling package can call `create_send_message_ticket()` (not version-gated) from their package with parameters, and call `send_message()` (version-gated) from a PTB so packages don't have to be updated during CCTP V1 package upgrades. * For USDC transfers, this function is called directly by the `TokenMessengerMinter` package in `deposit_for_burn()`. * `SendMessageTicket` also requires an `Auth` parameter. This is required in order to assign a `sender` to the message. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated `Auth` struct. Calling contracts should be careful to not expose these objects to the public or else their messages could be replaced. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). * The returned struct (`Message`) has the copy ability. There is also no guarantee of execution ordering, so your package could create 5 `SendMessageTickets` in one transaction and they could be executed in any order depending on the PTB. Integrating packages should account for both of these scenarios. **Parameters** | Field | Type | Description | | :-------------------- | :------------------------ | :---------------------------------------------------------------------------------------------------------- | | send\_message\_ticket | `SendMessageTicket` | A struct containing the necessary information to send a message created via `create_send_message_ticket()`. | | state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. | ### [send\_message\_with\_caller](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L85) Same as `send_message()` but with an additional parameter, `destination_caller`. This parameter specifies which address has permission to call `receive_message()` on the destination domain for the message. **Parameters** | Field | Type | Description | | :---------------------------------- | :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | | send\_message\_with\_caller\_ticket | `SendMessageWithCallerTicket` | A struct containing the necessary information to send a message created via `create_send_message_with_caller_ticket()`. | | state | `&mut State` | Shared `State` object for the `TokenMessengerMinter` package. | ### [replace\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L115https://github.com/circlefin/sui-cctp-private/blob/master/packages/message_transmitter/sources/send_message.move#L146) Replace a message with a new message body and/or destination caller. The `original_attestation` must be a valid attestation of `original_message`, produced by Circle's attestation service. **Remarks:** * The sender package of the replaced message must be the same as the caller of the original message. This is identified using the `Auth` generic parameter. See [stamp\_receipt](#stamp_receipt) for more info on `Auth` structs. **Parameters** | Field | Type | Description | | :----------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------- | | replace\_message\_ticket | `ReplaceMessageTicket` | A struct containing the necessary information to send a message created via `create_replace_message_ticket()`. | | state | `&mut State` | Shared `State` object for the `TokenMessengerMinter` package. | ## Additional Notes ### Destination Callers for Sui as Destination Chain Destination caller is a message field that specifies which address has permission to call `receive_message()` on the destination domain for the given message. On Sui this can either be an EOA (use `receive_message()`) or an `Auth` struct address for a package (use `receive_message_with_package_auth()`). Using a package destination caller allows integrators to run any atomic action in the same transaction that the message is received in. In order to determine the address to use for the destination caller field for Sui destination messages, please call `message_transmitter::auth::auth_caller_identifier()` with your `Auth` struct type. In order to use a package destination caller with Sui destination messages, integrators must create an `Auth` struct in their own package. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated `Auth` struct. Integrators should be careful to not expose these structs to the public or else messages with their package as destination caller could be received by others. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). ### Mint Recipient Addresses for Sui as Source Chain Outgoing mint recipient addresses from Sui are passed as Sui address types and can be treated the same as a `bytes32` mint recipient parameter on EVM implementations. ### Mint Recipient Addresses for Sui as Destination Chain Sui mint recipient addresses from other chains should be treated the same as a hex `bytes32` parameter. ### CCTP V1 Package Upgrades and Versioning CCTP V1 packages on Sui are upgradable. Public functions like `deposit_for_burn()`, `receive_message()`, etc. are version-gated. This means if the CCTP V1 packages are upgraded, the old versions of these functions will no longer be callable. Because of this, we do not recommend calling these functions directly from packages, and instead recommend calling the create ticket functions (not version-gated) directly from dependent packages, returning the created `Ticket` from your package, and then calling the main public function (e.g. `deposit_for_burn()` or `receive_message()`) from a PTB. By using the create ticket functions, dependent packages can securely set the parameters and `Auth` struct for the function call from within the package, and only have to update PTBs when CCTP V1 packages are upgraded. ### Integrating with CCTP V1 Sui from other Packages Integrating with the CCTP V1 Sui packages from other packages is different from non-Sui implementations. Rather than directly wrapping the CCTP-Sui packages like one would in Solidity, on Sui packages should interact with CCTP V1 packages in a more composable way. Third party packages should follow the `Ticket` pattern with a dedicated and private `Auth` struct as described below. #### Private Auth Structs `Auth` structs are used throughout the CCTP V1 packages in functions intended to be called from other dependent packages. The `auth_caller_identifier()` function is used to uniquely identify other packages by hashing the full object type of the type passed in. Any struct that implements the drop trait can be used as an authenticator, but it is recommended to use a dedicated auth struct. Calling contracts should be careful to not expose these structs to the public or else messages from their package could be forged. An example can be found in `TokenMessengerMinter` [on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move). #### Ticket Pattern The `Ticket` pattern is a pattern used in CCTP-Sui that enables the composability of CCTP V1 with third-party packages. The pattern enables a third-party integrator (package) to create a `Ticket` ("hot potato") for a designated operation directly in their package without having to upgrade their packages with future CCTP V1 upgrades. Only PTBs would need to be updated. `Ticket` structs contain parameters for specific interactions with CCTP V1 packages. They can only be created from and consumed by the CCTP V1 packages in a specific interaction, and do not have drop or store abilities, so must be used in the PTB where they are created. They also contain an `Auth` field that should only be created by the third-party package. The calling PTB should handle the `Ticket` by calling the relevant CCTP V1 package, which will recognize the third-party integrator as the action initiator. The following public functions (designed for third-party integrators, EOAs should use the entry versions) are implemented following the `Ticket` pattern. Each of them creates or consumes their own specific `Ticket` type: **`message_transmitter`:** * `receive_message_with_package_auth()` * `stamp_receipt()` **`token_messenger_minter`:** * `deposit_for_burn_with_package_auth()` * `deposit_for_burn_with_caller_with_package_auth()` * `replace_deposit_for_burn_with_package_auth()` For example, a typical workflow in a PTB to replace a deposit by an integrator would be: 1. The integrating package calls `create_replace_deposit_for_burn_ticket()` with an `Auth` struct it defined, and returns this ticket. 2. The PTB calls `deposit_for_burn_with_caller_with_package_auth()` with the ticket on behalf of the integrator. 3. `token_messenger_minter` will validate if the type hash of `Auth` matches the original sender in the burn message. #### PTB Function Call Ordering Due to the composability of Sui and PTBs, along with the `Ticket` pattern, there is no guarantee of ordering of calls within PTBs. The `Ticket` pattern introduces behaviors similar to asynchronous functions in ordinary programming contexts: when an integrator creates a ticket and returns it to the PTB, it is signaling an intention to execute the logic function, and the properties of the Move type system carry the guarantee that the function will indeed be eventually executed before the end of the transaction. However, no guarantee is given regarding the relative order of execution: the PTB is free to consume the tickets in any order it sees fit. While this has no security implications on the internal coherence of CCTP V1 itself, integrators should carefully evaluate whether their own logic is somehow dependent on a specific order of execution of the CCTP V1 functions. For example, a PTB could create 5 `DepositForBurnTicket` structs and execute them in any order. Similarly on the Sui destination side, 5 messages could be received in `MessageTransmitter`, and then received (and thus the USDC minted) in `TokenMessengerMinter` in a completely different order. If any pre or post actions are taken in third party packages, these could also come in an unexpected ordering, so this scenario should be handled accordingly in third party packages. #### Ticket Pattern Examples An example of this with receiving `deposit_for_burn()` messages on Sui can be seen below. This example assumes the `destination_caller` for the message is set to the auth address for your package's `Auth` struct. ```javascript JavaScript theme={null} // Prepare the ReceiveMessageTicket by calling create_receive_message_ticket() from within your package. let receive_msg_ticket = your_package::prepare_receive_message_ticket(message, attestation); // Receive the message on MessageTransmitter. let receipt = message_transmitter::receive_message_with_package_auth(receive_msg_ticket, ...); // Pass the Receipt into TokenMessengerMinter to mint the USDC. let ticket_with_burn_message = token_messenger_minter::handle_receive_message(receipt, ...); // In your package you can call deconstruct_stamp_receipt_ticket_with_burn_message to deconstruct the ticket // and burn_message and securely take some action with the burn_message (e.g. swap some tokens, send them somewhere, etc.) let stamp_receipt_ticket = your_package::take_some_action(ticket_with_burn_message, ...); // Stamp the receipt let stamped_receipt = message_transmitter::stamp_receipt(stamp_receipt_ticket); // Complete the message and destroy the StampedReceipt message_transmitter::complete_receive_message(stamped_receipt); ``` A similar example can be seen on the `deposit_for_burn()` side: ```javascript JavaScript theme={null} // Prepare the DepositForBurnWithCallerTicket by calling create_deposit_for_burn_with_caller_with_package_auth // directly from your package with the input parameters and your Auth struct. Integrators can also take other // actions here as needed. let deposit_for_burn_ticket = your_package::prepare_deposit_for_burn_ticket(coins, ...); // Call deposit for burn and burn the USDC let (burn_message, message) = token_messenger_minter::deposit_for_burn_with_caller_with_package_auth( deposit_for_burn_ticket, ... ); // Optionally, take some other action in your package based on the output message. // Note that BurnMessage and Message have the copy ability so the possibility of them being copied should be // handled in third party packages if post-actions are needed. your_package::post_deposit_for_burn(burn_message, message, ...); ``` # CCTP Chain Domains V1 Source: https://developers.circle.com/cctp/v1/supported-domains Mapping of supported CCTP V1 domains **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. A **domain** is a Circle-issued identifier for a blockchain where CCTP V1 contracts are deployed. Domains do not map to any existing public chain ID. ## CCTP V1 Domains | Domain | Name | | :----- | :---------- | | 0 | Ethereum | | 1 | Avalanche | | 2 | OP | | 3 | Arbitrum | | 4 | Noble | | 5 | Solana | | 6 | Base | | 7 | Polygon PoS | | 8 | Sui | | 9 | Aptos | | 10 | Unichain | # Transfer USDC on devnet between Solana and other chains using CCTP V1 Source: https://developers.circle.com/cctp/v1/transfer-testnet-usdc-between-solana-devnet Explore this tutorial for transferring USDC between Solana devnet and other testnets via CCTP V1 **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. To get started with CCTP V1 on Solana devnet, follow the [example scripts](https://github.com/circlefin/solana-cctp-contracts/tree/master/examples). The examples use [Solana web3.js](https://www.npmjs.com/package/@solana/web3.js) and [Anchor](https://www.npmjs.com/package/@project-serum/anchor) to transfer USDC to and from an account on Solana devnet and an address on an external blockchain. As a security measure, these scripts should only be used for devnet testing. You should not reuse private keys across devnet and mainnet. # Transfer USDC on testnet between Aptos and Base using CCTP V1 Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base Explore this tutorial for transferring USDC between Aptos testnet and Base Sepolia Testnet via CCTP V1 **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. To get started with CCTP V1 on Aptos testnet, follow the example scripts provided [on GitHub](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example). The examples use the [Aptos SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk), to transfer USDC to and from an address on Aptos testnet and an address on an external blockchain. **Do not reuse keys** As a security measure, these scripts should only be used on a testnet for testing purposes. It is not recommended to reuse private keys across mainnet and testnet. Summary of calling `deposit_for_burn()` (full runnable script can be found in the aptos-cctp repository): ```ts theme={null} // Aptos Testnet Stablecoin object const BURN_TOKEN = "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832"; const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); const userAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(APTOS_PRIVATE_KEY), }); // Create a transaction with deposit for burn script const buffer = readFileSync( "typescript/example/precompiled-move-scripts/testnet/deposit_for_burn.mv", ); const bytecode = Uint8Array.from(buffer); const amount = new U64(1); const destinationDomain = new U32(6); const burnToken = AccountAddress.from(BURN_TOKEN); const mintRecipient = AccountAddress.from(evmSigner.address); const functionArguments: Array = [ amount, destinationDomain, mintRecipient, burnToken, ]; const transaction = await aptosClient.transaction.build.simple({ sender: userAccount.accountAddress, data: { bytecode, functionArguments, }, }); const pendingTxn = await aptosClient.signAndSubmitTransaction({ signer: userAccount, transaction, }); const depositForBurnTx = await aptosClient.waitForTransaction({ transactionHash: pendingTxn.hash, }); console.log( `Deposit for burn transaction completed successfully: https://explorer.aptoslabs.com/txn/${depositForBurnTx.hash}`, ); // Fetch the event data from the transaction const messageSentEvent = ( depositForBurnTx as UserTransactionResponse ).events.find( (e: any) => e.type === `${MESSAGE_TRANSMITTER_PACKAGE_ID}::message_transmitter::MessageSent`, ); ``` Summary of calling `receive_message()` (full runnable script can be found in the aptos-cctp repository): ```ts theme={null} const bytecode = Uint8Array.from( fs.readFileSync( "typescript/example/precompiled-move-scripts/testnet/handle_receive_message.mv", ), ); const functionArguments: Array = [ MoveVector.U8(messageBytes as Buffer), MoveVector.U8(attestationSignature), ]; const transaction = await aptosClient.transaction.build.simple({ sender: userAccount.accountAddress, data: { bytecode, functionArguments, }, }); const pendingTxn = await aptosClient.signAndSubmitTransaction({ signer: userAccount, transaction, }); const receiveMessageTx = await aptosClient.waitForTransaction({ transactionHash: pendingTxn.hash, }); console.log( `Receive message transaction completed successfully: https://explorer.aptoslabs.com/txn/${receiveMessageTx.hash}`, ); ``` # Transfer USDC on testnet from Ethereum to Avalanche using CCTP V1 Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche Explore this script to transfer USDC on testnet between two EVM-compatible chains via CCTP V1 **This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**. This guide demonstrates how to use the [viem](https://viem.sh/) framework and the [CCTP V1 API](/cctp/v1/cctp-apis) in a simple script that enables a user to transfer USDC from a wallet address on the **Ethereum Sepolia testnet** to another wallet address on the **Avalanche Fuji testnet**. To get started with CCTP V1, follow the example script provided [on GitHub](https://github.com/circlefin/evm-cctp-contracts/blob/d1c24577fb627b08483dc42e4d8a37a810b369f7/docs/index.js). The example uses [web3.js](https://web3js.readthedocs.io/en/v1.8.1/getting-started.html) to transfer USDC from a wallet address on Ethereum Sepolia testnet to another wallet address on Avalanche Fuji testnet. The script has five steps: 1. In this first step, you initiate a transfer of USDC from one blockchain to another, and specify the recipient wallet address on the destination chain. This step approves the Ethereum Sepolia **TokenMessenger** contract to withdraw USDC from the provided Ethereum Sepolia wallet address. ```javascript JavaScript theme={null} const approveTx = await usdcEthContract.methods .approve(ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS, amount) .send({ gas: approveTxGas }); ``` 2. In this second step, you facilitate a burn of the specified amount of USDC on the source chain. This step executes the `depositForBurn` function on the Ethereum Sepolia **TokenMessenger** contract deployed on [Sepolia testnet](https://sepolia.etherscan.io/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5). ```javascript JavaScript theme={null} const burnTx = await ethTokenMessengerContract.methods .depositForBurn( amount, AVAX_DESTINATION_DOMAIN, destinationAddressInBytes32, USDC_ETH_CONTRACT_ADDRESS, ) .send(); ``` 3. In this third step, you make sure you have the correct message and hash it. This step extracts `messageBytes` emitted by the **MessageSent** event from `depositForBurn` transaction logs and hashes the retrieved `messageBytes` using the **keccak256** hashing algorithm. ```javascript JavaScript theme={null} const transactionReceipt = await web3.eth.getTransactionReceipt( burnTx.transactionHash, ); const eventTopic = web3.utils.keccak256("MessageSent(bytes)"); const log = transactionReceipt.logs.find((l) => l.topics[0] === eventTopic); const messageBytes = web3.eth.abi.decodeParameters(["bytes"], log.data)[0]; const messageHash = web3.utils.keccak256(messageBytes); ``` 4. In this fourth step, you request the attestation from Circle, which provides authorization to mint the specified amount of USDC on the destination chain. This step polls the attestation service to acquire the signature using the `messageHash` from the previous step. **Rate Limit** The attestation service rate limit is 35 requests per second. If you exceed 35 requests per second, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 response. ```javascript JavaScript theme={null} let attestationResponse = { status: "pending" }; while (attestationResponse.status != "complete") { const response = await fetch( `https://iris-api-sandbox.circle.com/attestations/${messageHash}`, ); attestationResponse = await response.json(); await new Promise((r) => setTimeout(r, 2000)); } ``` 5. In this final step, you enable USDC to be minted on the destination chain. This step calls the `receiveMessage` function on the Avalanche Fuji **MessageTransmitter** contract to receive USDC at the Avalanche Fuji wallet address. ```javascript JavaScript theme={null} const receiveTx = await avaxMessageTransmitterContract.receiveMessage( receivingMessageBytes, signature, ); ``` You have successfully transferred USDC between two EVM-compatible chains using CCTP V1 end-to-end. # Transfer USDC on testnet between Noble and Ethereum using CCTP V1 Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-noble-to-ethereum Explore this tutorial for transferring USDC on testnet between Noble via CCTP V1 and Ethereum **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. To transfer USDC between Noble testnet and Ethereum Sepolia, follow the tutorials provided [on GitHub](https://github.com/circlefin/noble-cctp/tree/master/examples). Specifically, follow the instructions for the [DepositForBurn script](https://github.com/circlefin/noble-cctp/blob/master/examples/depositForBurn.ts) to test transferring USDC from Noble testnet to Ethereum Sepolia, and the instructions for the [ReceiveMessage script](https://github.com/circlefin/noble-cctp/blob/master/examples/receiveMessage.ts) to transfer USDC from Ethereum Sepolia to Noble testnet. As a security measure, these scripts should only be used for testnet testing. It is not recommended to reuse private keys across mainnet and testnet. **Note:** This tutorial relies on the Strangelove Ventures [Noble CCTP V1 relayer](https://github.com/strangelove-ventures/noble-cctp-relayer), which is a service that automatically calls `receiveMessage()` for messages transmitted to and from Noble domains. To avoid relying on this relayer, you can submit this `receiveMessage()` transaction directly. (If you do not want your transaction relayed automatically, you can specify a `destinationCaller` via `depositForBurnWithCaller()`.) # Transfer USDC on testnet between Sui and Ethereum using CCTP V1 Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum Explore this tutorial for transferring USDC between Sui testnet and Ethereum Sepolia Testnet via CCTP V1 **This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**. To get started with CCTP V1 on Sui testnet, follow the example scripts provided [on GitHub](https://github.com/circlefin/sui-cctp/tree/master/scripts/sui-scripts). The [README](https://github.com/circlefin/sui-cctp?tab=readme-ov-file#run-localnet-example-scripts) contains instructions for running the scripts. The examples use the [Sui SDK](https://www.npmjs.com/package/@mysten/sui), to transfer USDC to and from an address on Sui testnet and an address on an external blockchain. **Do not reuse keys** As a security measure, these scripts should only be used on a testnet for testing purposes. It is not recommended to reuse private keys across mainnet and testnet. Summary of calling `deposit_for_burn()` (full runnable script can be found in the sui-cctp repository): ```javascript JavaScript theme={null} // Create DepositForBurn tx const depositForBurnTx = new Transaction(); // Split USDC to send in depositForBurn call const ownedCoins = await client.getAllCoins({owner: signer.toSuiAddress()}) const usdcStruct = ownedCoins.data.find(c => c.coinType.includes(usdcId)); if (!usdcStruct || Number(usdcStruct.balance) < USDC_AMOUNT) { throw new Error("Insufficient tokens in wallet to initiate transfer."); } const [coin] = depositForBurnTx.splitCoins( usdcStruct.coinObjectId, [USDC_AMOUNT] ); // Create the deposit_for_burn move call depositForBurnTx.moveCall({ target: `${tokenMessengerMinterId}::deposit_for_burn::deposit_for_burn`, arguments: [ depositForBurnTx.object(coin), // Coin depositForBurnTx.pure.u32(DESTINATION_DOMAIN), // destination_domain depositForBurnTx.pure.address(evmUserAddress), // mint_recipient depositForBurnTx.object(tokenMessengerMinterStateId), // token_messenger_minter state depositForBurnTx.object(messageTransmitterStateId), // message_transmitter state depositForBurnTx.object("0x403"), // deny_list id, fixed address depositForBurnTx.object(treasuryId) // treasury object Treasury ], typeArguments: [`${usdcId}::usdc::USDC`], }); // Broadcast the transaction console.log("Broadcasting sui deposit_for_burn tx..."); const depositForBurnOutput = await executeTransactionHelper({ client: client, signer: signer, transaction: depositForBurnTx, }); assert(!depositForBurnOutput.errors); console.log(`deposit_for_burn transaction successful: 0x${depositForBurnOutput.digest} \n`); // Get USDC balance changes (optional) const suiUsdcBalanceChange = depositForBurnOutput.balanceChanges?.find(b => b.coinType.includes(usdcId)) const balances = await client.getAllBalances({ owner: signer.toSuiAddress() }); const usdcBalance = balances.find(b => b.coinType.includes(usdcId))?.totalBalance; // Get the message emitted from the tx const messageRaw: Uint8Array = (depositForBurnOutput.events?.find((event) => event.type.includes("send_message::MessageSent") )?.parsedJson as any).message; const messageBuffer = Buffer.from(messageRaw); const messageHex = `0x${messageBuffer.toString("hex")}`; const messageHash = web3.utils.keccak256(messageHex); console.log(`Message hash: ${messageHash}`); ``` Summary of calling `receive_message()` (full runnable script can be found in the sui-cctp repository): ```javascript JavaScript theme={null} // Create receiveMessage transaction const receiveMessageTx = new Transaction(); // Add receive_message move call to MessageTransmitter const [receipt] = receiveMessageTx.moveCall({ target: `${messageTransmitterId}::receive_message::receive_message`, arguments: [ receiveMessageTx.pure.vector( "u8", Buffer.from(evmBurnTx.message.replace("0x", ""), "hex"), ), // message as byte array receiveMessageTx.pure.vector( "u8", Buffer.from(attestation.replace("0x", ""), "hex"), ), // attestation as byte array receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state ], }); // Add handle_receive_message call to TokenMessengerMinter with Receipt from receive_message call const [stampReceiptTicketWithBurnMessage] = receiveMessageTx.moveCall({ target: `${tokenMessengerMinterId}::handle_receive_message::handle_receive_message`, arguments: [ receipt, // Receipt object returned from receive_message call receiveMessageTx.object(tokenMessengerMinterStateId), // token_messenger_minter state receiveMessageTx.object("0x403"), // deny list, fixed address receiveMessageTx.object(treasuryId), // usdc treasury object Treasury ], typeArguments: [`${usdcId}::usdc::USDC`], }); // Add deconstruct_stamp_receipt_ticket_with_burn_message call const [stampReceiptTicket] = receiveMessageTx.moveCall({ target: `${tokenMessengerMinterId}::handle_receive_message::deconstruct_stamp_receipt_ticket_with_burn_message`, arguments: [stampReceiptTicketWithBurnMessage], }); // Add stamp_receipt call const [stampedReceipt] = receiveMessageTx.moveCall({ target: `${messageTransmitterId}::receive_message::stamp_receipt`, arguments: [ stampReceiptTicket, // Receipt ticket returned from deconstruct_stamp_receipt_ticket_with_burn_message call receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state ], typeArguments: [ `${tokenMessengerMinterId}::message_transmitter_authenticator::MessageTransmitterAuthenticator`, ], }); // Add complete_receive_message call to MessageTransmitter with StampedReceipt from stamp_receipt call. // Receipt and StampedReceipt are Hot Potatoes so they must be destroyed for the // transaction to succeed. receiveMessageTx.moveCall({ target: `${messageTransmitterId}::receive_message::complete_receive_message`, arguments: [ stampedReceipt, // Stamped receipt object returned from handle_receive_message call receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state ], }); // Broadcast the transaction console.log("Broadcasting Sui receive_message tx..."); const receiveMessageOutput = await executeTransactionHelper({ client: client, signer: signer, transaction: receiveMessageTx, }); ``` # Circle Mint Source: https://developers.circle.com/circle-mint Mint and redeem USDC and EURC directly from Circle. Circle Mint is for institutional customers minting USDC or EURC. It is typically used by exchanges, institutional traders, wallet providers, banks, and consumer-app companies. [Contact Circle](https://www.circle.com/mint-contact) to learn more. Circle Mint lets you mint (convert fiat to stablecoins) and redeem (convert stablecoins back to fiat) [USDC](/stablecoins/what-is-usdc) and EURC directly from Circle, the sole issuer of both stablecoins. Every token redeems 1:1 for its backing fiat currency: U.S. dollars for USDC, euros for EURC. Manage your account through the [Mint Console](https://app.circle.com/) or the Circle Mint API. Deposit fiat from a linked bank account, convert it to USDC or EURC, and send stablecoins to blockchain wallets globally. To start integrating, [set up your account and API key](/circle-mint/quickstarts/getting-started). ## What you can do Convert fiat to USDC or EURC and redeem stablecoins back to fiat through your Circle Mint account. Send and receive USDC and EURC on supported blockchains. Use Circle's Payment APIs to accept USDC deposits from your customers. Convert between local currencies and USDC using cross-currency APIs. # Additional APIs Source: https://developers.circle.com/circle-mint/additional-apis Circle Mint customers can access extra APIs to use USDC in their apps. Circle Mint customers can access extra APIs to use USDC in their apps. These APIs cost extra. Your use case might need more support from Circle. Contact your Circle representative or [sales@circle.com](mailto:sales@circle.com) for more information. ## Move money onchain Use the Crypto Deposits and Crypto Payouts APIs to manage your Mint account. These APIs let you build custom software that performs Circle Mint actions. * Accept stablecoin payins: Take USDC and EURC on [supported blockchains](/circle-mint/references/supported-chains-and-currencies#crypto-deposits-api-and-crypto-payouts-api) through payment intents. * Make stablecoin payouts: Send fast, low-cost onchain payouts to customers, vendors, and suppliers. ### Stablecoin payins With the Crypto Deposits API, you take USDC and EURC through onchain transfers into your Mint wallet using payment intents. For details, see [How Stablecoin Payins and Payouts Work](/circle-mint/how-stablecoin-payins-and-payouts-work). Read the quickstart to [receive a stablecoin payin](/circle-mint/receive-stablecoin-payin). ### Stablecoin payouts The Crypto Payouts API sends payouts to customers, vendors, or suppliers on supported blockchains. You can fund payouts in USDC with your Circle Mint account. Your account can receive deposits from traditional and blockchain payment rails. Read the quickstart to [send stablecoin payouts](/circle-mint/send-stablecoin-payout). ## Currency exchange The Currency Exchange API lets you swap local currency for USDC and USDC for EURC. [Read the quickstart guide on the Cross-Currency API](/circle-mint/exchange-local-currency-usdc). ## Credit API The Credit API provides programmatic access to Circle Mint's credit products: Settlement Advance and Line of Credit. You can initiate draws, track transfer status, manage fees, and process repayments through a dedicated set of endpoints under `/v1/credit/`. * **Settlement Advance**: Reserve funds, upload wire proof, and receive disbursement after Treasury approval. * **Line of Credit**: Request draws with auto-approval and repay outstanding balances using USDC from your Circle Mint wallet. [Read the Credit API overview](/circle-mint/credit-api/overview). # API Logs Source: https://developers.circle.com/circle-mint/api-logs View API log entries in the Developer Tab to review transactions and debug any errors. *[API Logs](https://app-sandbox.circle.com/developer/logs) are transaction logs that enable you to view your API transaction history and debug API errors with no setup required. They show the most accurate representation of what Circle received and sent. When you send an API request, Circle stores it along with its associated response. On the API logs page, you can view these API logs for seven days after the request is sent and filter them to find the specific request you are looking for. You can use the API logs to debug API errors without having to log out of your system.* * To access the logs, go to [Circle Sandbox](https://app-sandbox.circle.com) or [Circle Mint](https://app.circle.com) and click the **Developer Tab > Logs**. ## API Log Data Elements For each API request and response, Circle stores the following information, which you can retrieve and review in the Developer tab. | Field | Description | | :------------ | :---------------------------------------------------------------------------------------------------------------------- | | HTTP Status | HTTP Status code for each request such as 200 or 400. | | Path | The path excludes the base URL. | | Request ID | The `X-Request-Id` in the header provided in the request or is generated plus by Circle and returned in the response. | | User Agent | The User-Agent field in the request header. It is common that the HTTP library used will provide this field by default. | | Idempotency | Idempotency Key sent in the request body. This is only found in POST requests. | | Origin | Includes the protocol (HTTP/HTTPS), the domain or IP address, and the port number if applicable. | | Time | Timestamp of when the request was received. | | Request Body | The full request body. | | Response Body | The full response body. | ## API Log Filtering To filter your search, use the search fields and popup menus at the top of the Circle Sandbox or Circle Mint page. | Filter Name | Description | | :---------- | :------------------------------------------------------------------------------------------------------------- | | Search | Filter by request ID, resource ID, or idempotency key ID. | | Date Range | Filter results by date range. | | Status | Filter results by `succeeded` and/or `failed`. Succeeded includes all 2## and failed includes all 4## and 5##. | | Method | Filter results by HTTP Method. Supports `POST`, `PUT`, `PATCH`, `DELETE`, and/or `GET`. | | Path | Filter results by the URL path such as `/payments`. | ## Data Redaction Due to the sensitivity of some data provided to Circle, including financial payment methods and PII data, some values will be redacted from the request and response payloads. Values that are redacted are replaced with the value `“[redacted].”` If you need access to this sensitive data, reach out to your Circle customer success manager. # CAMT.053 Daily Statements for Reconciliation Source: https://developers.circle.com/circle-mint/camt053-statements Integrate ISO 20022 CAMT.053 daily statement XML from Circle Mint for treasury and reconciliation workflows. [ISO 20022](https://www.iso20022.org/) CAMT.053 is a standard bank-to-customer statement format used for treasury reconciliation. Circle Mint generates one CAMT.053.001.13 XML file per calendar day (UTC), covering all USDC and EURC activity on your account. You [retrieve the file](/circle-mint/camt053-statements#retrieve-the-report) through the Circle Mint API and parse it to reconcile balances, match transactions, and integrate with your treasury systems. ## Availability and SLA * **Coverage**: Each report covers the prior calendar day from 00:00 through 24:00 UTC. * **When it is ready**: The report is committed to be available by 06:00 UTC the next day. Typically, it is ready around 02:00 UTC. * **One file, multiple currencies**: USDC and EURC statements are delivered in a single XML file as separate `Stmt` blocks. Subscribe to the [Circle status page](https://status.circle.com/) to receive updates on CAMT.053 outages. ## Retrieve the report Retrieve the CAMT.053 report by first calling [Request a report](/api-reference/circle-mint/account/request-report) with `reportType: camt053` and `date` in `YYYY-MM-DD` format (the UTC calendar date the statement covers), then use the returned `id` with one of the following endpoints: * [Get report by ID](/api-reference/circle-mint/account/get-report-by-id): Returns JSON with a `data` object that includes `downloadUrl` and `expiresAt`. Download the XML from `downloadUrl` before the time indicated by `expiresAt`. * [Get report content](/api-reference/circle-mint/account/get-report-content): Returns the raw XML as `application/xml`. The response uses a `Content-Disposition` attachment filename such as `camt053_YYYY-MM-DD_report.xml`. ## Read and reconcile the report The report follows the ISO 20022 CAMT.053.001.13 structure. The sections below cover the key elements you need for reconciliation. ### File format at a glance The report uses ISO 20022 CAMT.053.001.13 XML structure. See the [ISO 20022 message definitions catalog](https://www.iso20022.org/iso-20022-message-definitions) for official message definitions. * The file uses namespace `urn:iso:std:iso:20022:tech:xsd:camt.053.001.13`. * The root is `Document` → `BkToCstmrStmt` → one or more `Stmt` elements. * Each `Stmt` is one account and currency. * Inside a statement you will see opening and closing balances and `Ntry` (entry) elements. There is one `Ntry` per movement that affects the balance. ### Balances Each `Stmt` includes opening and closing balances for the report date: * `OPBD`: Opening booked balance for the report date. * `CLBD`: Closing booked balance for the report date. Each balance has an amount with `Ccy` (currency), `CdtDbtInd` (credit or debit side of the balance), and a timestamp. For reconciliation, a common check is: opening balance plus the sum of entries (respecting credit and debit) aligns with closing balance for that statement. ### Currency vs token Standard ISO 20022 fields follow ISO 4217. They use three-letter currency codes on `Amt` and related standard elements: * `USD` for USDC transactions (for example ``). * `EUR` for EURC transactions (for example ``). The full token identifier (`USDC` or `EURC`) is preserved on each transaction line. At the transaction level it follows the path `Ntry` → `NtryDtls` → `TxDtls` → `SplmtryData` → [`CircleTxn`](#circle-transaction-details-circletxn) → `Token`. Use both when you need to match fiat-style accounting codes and token identifiers. ### Transaction entries (``) Each `Ntry` is one transaction line. Important children include: * `Amt`: Amount and ISO currency on the amount (`USD` or `EUR`). * `CdtDbtInd`: `CRDT` (credit) or `DBIT` (debit). * `Sts`: Booking status in `Cd`, such as: * `BOOK`: Booked * `PDNG`: Pending * `RJCT`: Rejected * `FAIL`: Failed * `BookgDt`: Booking time in `DtTm`, UTC (ISO 8601). * `BkTxCd`: Circle's label for the transaction type, found under `Prtry` / `Cd`. New labels can appear over time. Unmapped types may show as `UNKNOWN`. ### Circle transaction details (`CircleTxn`) Circle adds detail under `Ntry` → `NtryDtls` → `TxDtls` → `SplmtryData` where `PlcAndNm` is `CircleTransactionData`. Inside the envelope, `CircleTxn` uses namespace `urn:circle:camt053:transaction`. The following child elements may appear on a transaction line. This list is not exhaustive — your parser should tolerate additional elements and fields that are omitted. * `TransactionId`: Identifier for the movement. * `JobId`: Related job identifier. * `Token`: Full token identifier (`USDC` or `EURC`). * `CustomReferenceId`: Customer-provided reference when supplied. * `ExternalReferenceId`: EFT-style reference when supplied (for example `IMAD` or `UETR`). * `Blockchain`: Blockchain identifier when the movement is onchain. * `TransactionHash`: Onchain transaction hash when present. * `Source`: Originating party or address when populated. * `SourceType`: Type of source (for example fiat account or blockchain address) when populated. * `Destination`: Receiving party or address when populated. * `DestinationType`: Type of destination when populated. * `CustomerId`: Customer association when present. Use these fields to tie a line back to APIs, onchain activity, or internal references. ## Example truncated statement The example below shows the header, one statement with opening and closing balances, and one entry (abbreviated). It is only for orientation. Your real files can contain many entries and a second statement for the other currency. ```xml Example CAMT.053 fragment theme={null} CAMT053_0455_20251006 2025-10-07T14:30:45Z e0549c6e-c80e-4e5f-95ee-c66f7d1be455 EntityId 0455_1000123456_USD_20251006 1000123456 USD OPBD 1750000.00 CRDT
2025-10-06T00:00:00Z
CLBD 1775000.00 CRDT
2025-10-06T23:59:59Z
25000.00 CRDT BOOK 2025-10-06T08:15:23Z Mint wire CircleTransactionData 550e8400-e29b-41d4-a716-446655440000 660e9511-f3ac-52e5-b827-557766551111 USDC
``` # API Resource Data Models Source: https://developers.circle.com/circle-mint/circle-api-resources Discover the many resources used by Circle APIs to enable payouts, payments, and transfers. Circle APIs are a set of functions and procedures that allow you to use USDC for payments, payouts, and transfers. Circle's APIs are built around several resources that represent payments, payouts, transfers, and their associated objects. ## Primary resources ### Payout object A `payout` object represents a payout to a customer, vendor, or supplier. #### Example ```json JSON theme={null} { "id": "df3b8e5f-9579-4c1f-9fa9-deac7f4be55c", "sourceWalletId": "1000038499", "destination": { "id": "4d260293-d17c-4309-a8da-fa7850f95c10", "type": "address_book", }, "amount": { "amount": "10.0", "currency": "USD" }, "fees": { "amount": 3.0, "currency": "USD" }, "status": "complete", "errorCode": "insufficient_funds", "riskEvaluation": { "decision": "denied", "reason": "3000" }, "return": { ... }, "createDate": "2020-12-24T11:19:20.561Z", "updateDate": "2020-12-24T12:01:00.523Z", } ``` #### Attributes *** **`id`** *string* A `UUID` for the payout. *** **`sourceWalletId`** *string* The identifier of the source wallet used to fund a payout. *** **`destination`** *object* The [Destination object](#source-and-destination-objects) the payout is being made to. *** **`amount`** *object* A [Money object](#money-object) representing the total amount that is paid to the destination. *** **`toAmount`** *object* A [Money object](#money-object) representing the amount that is paid to the destination currency. Only included for stablecoin payouts. *** **`fees`** *object* A [Money object](#money-object) representing fees associated with this payment. *** **`status`** *string* Status of the payout. `pending` indicates that the payout is in process; `complete` indicates it finished successfully; `failed` indicates it failed. *** **`externalRef`** *string* External network identifier which will be present once provided from the applicable network. *** **`errorCode`** *string* Indicates the failure reason of a payout. Only present for payouts in failed state. Possible values are `insufficient_funds`, `transaction_denied`, `transaction_failed`, and `transaction_returned`. *** **`riskEvaluation`** *object* An object with two attributes, `decision` and `reason`. *** **`return`** *object* A Return object if the payout was returned. *** **`createDate`** *string* ISO-8601 UTC date/time format. *** **`updateDate`** *string* ISO-8601 UTC date/time format. ### Transfer object A `transfer` object represents a transfer of funds from a Circle wallet to a blockchain address, from a blockchain address to a Circle wallet, or between two Circle wallets. #### Example ```json JSON theme={null} { "id": "0d46b642-3a5f-4071-a747-4053b7df2f99", "source": { "type": "blockchain", "chain": "ETH", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F" }, "destination": { "type": "wallet", "id": "12345" }, "amount": { "amount": "3.14", "currency": "USD" }, "transactionHash": "0x4cebf8f90c9243a23c77e4ae20df691469e4b933b295a73376292843968f7a63", "status": "pending", "riskEvaluation": { "decision": "approved", "reason": "1234" }, "createDate": "2020-04-10T02:29:53.888Z" } ``` #### Attributes *** **`id`** *string* A `UUID` for the transfer. *** **`source`** *object* A [Source object](#source-and-destination-objects) representing the source of the transfer. *** **`destination`** *object* A [Destination object](#source-and-destination-objects) representing the destination of the transfer. *** **`amount`** *object* A [Money object](#money-object) representing the amount transferred between source and destination. *** **`transactionHash`** *string* A hash that uniquely identifies an onchain transaction. This is only available when either `source` or `destination` are of type `blockchain`. *** **`status`** *string* Status of the transfer. Status `pending` indicates that the transfer is in the process of running; `complete` indicates it finished successfully; `failed` indicates it failed. *** **`createDate`** *string* ISO-8601 UTC date/time format. *** ## Nested resources The following are resources that are commonly used in other objects. ### Source and destination objects Payments, payouts, and transfers reference `source` and `destination` objects, which as the names suggest, tell you where the funds are coming from and where they're going. Sources and destinations can have the following types: * `wire` for wire payments and payouts * `blockchain` for transfers to/from blockchain addresses * `wallet` for transfers to/from a Circle `wallet` ```json JSON theme={null} // "blockchain" // A "chain" and "address" together represent the blockchain location. { "type": "blockchain", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "chain": "ETH" } // "wallet" // The "id" is the id of the Circle wallet. { "type": "wallet", "id": "12345" } ``` ### Money object Monetary amounts across Circle APIs are represented as `money` objects, which consist of an `amount` and a `currency`. Two currencies are supported, `USD` and `EUR`, with the amount represented as a string containing the whole units and two decimals. In the example below, the amount is represented as 3.14 and the currency as `USD`. ```json JSON theme={null} { "amount": "3.14", "currency": "USD" } ``` ### Blockchain addresses ```json JSON theme={null} { "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "currency": "USD", "chain": "ETH" } ``` # Circle APIs: API and Entity Errors Source: https://developers.circle.com/circle-mint/circle-apis-api-errors Circle APIs use two kinds of error codes: * **API response errors** return right away with the HTTP response. * **Entity errors** attach to resources such as payments. They usually show up seconds or minutes later. ## Error responses When an API request is made and the error is known immediately, Circle APIs will return the appropriate HTTP status code along with a JSON error response. Because HTTP status codes do not always provide sufficient information about the cause of an error, Circle's API errors provide more detailed JSON responses to help you determine what's gone wrong. In the example below, the request is missing a required field. The HTTP status code in this case would be `HTTP/1.1 400 Bad Request` ```json JSON theme={null} { "code": 2, "message": "Invalid entity. metadata.email may not be empty (was null)", "errors": [ { "error": "invalid_value", "location": "metadata.email", "message": "metadata.email may not be empty (was null)" } ] } ``` ### Error types You can automate the handling of certain error types. These include the following error names, which include example messages and constraints, where applicable: | error | message | constraints | | :---------------------- | :---------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | | `value_must_be_true` | field1 must be true (was false) | NA | | `value_must_be_false` | field1 must be false (was true) | NA | | `required` | field1 may not be null (was null) | NA | | `not_required` | field1 must be null (was foo) | NA | | `min_value` | field1 must be greater than or equal to 2 (was 0) | `"constraints": { "min": 2 }` | | `min_value` | field1 must be greater than 2.0 (was 2.0) | `"constraints": { "min": "2.0", "inclusive": false }` | | `max_value` | field1 must be less than or equal to 99 (was 1024) | `"constraints": { "max": 99 }` | | `max_value` | field1 be less than or equal to 99.99 (was 100) | `"constraints": { "max": "99.99", "inclusive": true }` | | `length_outside_bounds` | field1 must be between 2 and 5 (was qwerty) | `"constraints": { "min": 2, "max": 5 }` | | `pattern_mismatch` | field1 must match "\[a-z]+" (was 123456) | `"constraints": { "pattern": "[a-z]+" }` | | `date_not_in_past` | field1 must be in the past (was 2020-08-31T18:18:54.211Z) | NA | | `date_not_in_future` | field1 must be in the future (was 2020-08-31T18:18:17.621Z) | NA | | `number_format` | field1 numeric value out of bounds (\<3 digits>.\<2 digits> expected) (was 123.456) | `"constraints": { "max-integral-digits": 3, "max-fractional-digits": 2 }` | ### API error format Whenever an API request results in an error, the JSON response contains both an error code and a human-readable message in the body. ```json JSON theme={null} { "code": 2, "message": "Invalid entity.\n[...\n]" } ``` ### Extended error format In some cases, you'll receive extended information about why a request has failed. For example, if you fail to supply a value for a required field, you'll receive the following error response: ```json JSON theme={null} { "code": 2, "message": "Invalid entity.\nfield1 may not be null (was null)", "errors": [ { "error": "required", "message": "field1 may not be null (was null)", "location": "field1", "invalidValue": "null", "constraints": {} } ] } ``` This extended error response contains one or more associated error descriptions. Error description attributes can be read as follows: | Key | Description | optional | | :------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------- | | `error` | type of an error | false | | `message` | human-friendly message | false | | `location` | period-separated path to the property that causes this error. An example could be `field1` or `address.billingCountry` | false | | `invalidValue` | actual value of the property specified in `location` key, as received by the server | true | | `constraints` | special object that contains additional details about the error and could be used for programmatic handling on the client side | true | ### List of API error codes The table below shows the list of JSON error codes that may be returned in an API error response. | Code | Message | Description | | -------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `-1` | Unknown Error | An unknown error occurred processing the API request | | `1` | Malformed authorization. Is the authorization type missing? | API Key is missing or malformed | | `2` | Invalid Entity | Error with the JSON object passed in the request body | | `3` | Forbidden | API Key used with request does not have one of the roles authorized to call the API endpoint | | `1077` | Payment amount is invalid | Payment amount must be greater than zero | | `1078` | Payment currency not supported | An invalid currency value was used when making a payment | | `1083` | Idempotency key already bound to other request | The idempotency key used when making a request was used with another payment. Retry with a different value | | `1084` | This item cannot be canceled | A cancel or refund request cannot be canceled | | `1085` | This item cannot be refunded | A cancel or refund request cannot be refunded | | `1086` | This payment was already canceled | This payment was already canceled | | `1087` | Total amount to be refunded exceeds payment amount | Total amount to be refunded exceeds payment amount | | `1088` | Invalid source account | An invalid source account was specified in a payout or transfer request | | `1089` | The source account could not be found | Unable to find the source account specified in a payout or transfer request | | `1093` | Source account has insufficient funds | The source account has insufficient funds for the payout or transfer amount | | `1096` | Encryption key id could not be found | An encryption key id must be provided if request includes encrypted data | | `1097` | Original payment is failed | Attempting to cancel or refund a failed payment | | `1101` | Invalid country format | An invalid ISO 31660-2 country code was provided | | `1106` | Invalid district format | Invalid district format, must be a 2 character value | | `1107` | Payout limit exceeded | Payout can't be accepted as it would exceed the given limit | | `1108` | Unsupported Country | Country not supported for customer | | `1143` | Checkout session not found | The checkout session id passed in the request doesn't exist in the DB | | `1144` | Checkout session is already in a completed state | The checkout session cannot be extended because it is already in a complete state | | `2003` | The recipient address already exists | The blockchain address has already been associated with the account | | `2004` | The address is not a verified withdrawal address | The blockchain address must first be verified before it can be used as a destination in a transfer request | | `2005` | The address belongs to an unsupported blockchain | The blockchain type used as a transfer destination is not supported | | `2006` | Wallet type is not supported | The wallet type specified when creating an end user wallet is not supported | | `2007` | Unsupported transfer | A transfer from the provided source to the provided destination is not supported | | `2024` | Address book identity is missing | Address book identity is required for this entity | | `2025` | Address book ownership is missing | Address book ownership is required for this entity | | `2026` | Address book VASP ID is missing | Address book VASP ID is required for this entity | | `2027` | Address book ownership type is invalid | Address book ownership type is not supported for this entity | | `2028` | Address book custody type is invalid | Address book custody type is not supported for this entity | | `2029` | Address book custody is missing | Address book custody is required for this entity | | `2030` | Address book VASP ID is invalid | Address book VASP ID is not valid for this entity | | `2031` | Address book identity type is invalid | Address book identity type must be either individual or business | | `2032` | Address book identity first name is missing | First name is required for individual identity | | `2033` | Address book identity last name is missing | Last name is required for individual identity | | `2034` | Address book identity business name is missing | Business name is required for business identity | | `2035` | Address book VASP ID is not allowed | VASP ID must not be provided for self-hosted custody | | `2036` | Identity is not allowed in address book patch requests | The identity field cannot be updated via the patch endpoint | | `2037` | Ownership is not allowed in address book patch requests | The ownership field cannot be updated via the patch endpoint | | `5000` | Invalid travel rule identity type | The provided identity type must be either "individual" or "business" | | `5001` | Payout not found | Payout doesn't exist based on the ID provided. Verify the payout ID. | | `5002` | Invalid payout amount | Payout amount must be more than 0 | | `5003` | Inactive destination address | Cannot send payout to an inactive destination address. If you have just added the address, you may have to wait for 24 hours before use | | `5004` | Destination address not found | The destination address for this payout could not be found | | `5005` | Source wallet not found | Source wallet for this payout could not be found | | `5006` | Insufficient funds | The source wallet has insufficient funds for this payout | | `5007` | Unsupported currency | Currency not supported for this operation | | `5011` | Invalid destination address | Can't send payout to an invalid destination address | | `5012` | Invalid destination location types | Can't search for both crypto and fiat payouts | | `5013` | Invalid source wallet id | Source wallet id must be a number for payouts search | | `5014` | The address is not valid for the blockchain | Provided blockchain address is not valid for the corresponding blockchain | | `5015` | Invalid destination chain | Provided blockchain address has an invalid chain in respect to the currency used | | `5020` | Invalid purpose of transfer | The `purposeOfTransfer` field is missing or invalid for crypto payouts | | `500000` | Unsupported currency for reporting daily user balance | User provided a currency other than USDC or EURC | | `500001` | Custody balance `asOfDate` is too far from today's date | `asOfDate` provided was before or after today's date | | `500002` | Custody balance cannot be less than zero | Negative balances were provided | | `500003` | Local custody balance can't be greater than total custody balance | Total balance must be greater than or equal to local balance | | `500004` | Idempotency key can't be reused | The same idempotency key was used for different requests | | `500005` | Custody balance report already exists | A custody balance report was already created for the provided date and currency. Contact customer care for edits. | ## Entity error responses With entities such as payments, cards, bank wires, and transfers, an error is generally not known immediately, so no error code can be returned at the time of the request. For instance, when making a payment, the transaction is processed asynchronously and the create payment request will have a status of `pending`. After processing, the status will eventually be set to `approved` or `failed`. API entities such as payments and cards are processed asynchronously after the initial create request is made. If a problem occurs while processing the request (the status is shown as `failed`), the `errorCode` property is set on the entity and can be retrieved either by polling the `GET` endpoint or via a notification. The response explains why the payment was unsuccessful. Here are some Entity error codes associated with payments, cards, bank wires, and transfers. ### Payment error codes | Code | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------ | | `payment_failed` | Payment failed due to an unknown reason | | `payment_fraud_detected` | Suspected fraud detected by issuing bank. Instruct your user to contact their bank directly to resolve | | `payment_denied` | Payment denied by Circle Risk Service, see payment `riskReasonCode` for more details | | `payment_not_supported_by_issuer` | Issuer bank was unable to process the transaction | | `payment_not_funded` | There were insufficient funds to cover the payment amount | | `payment_unprocessable` | The provided `encryptedData` could not be processed | | `payment_stopped_by_issuer` | A stop has been placed by the issuer or customer | | `payment_canceled` | Payment was canceled | | `payment_returned` | Payment was returned | | `payment_failed_balance_check` | Payment failed the Plaid balance check due to insufficient funds | | `card_failed` | Card payment failed | | `card_invalid` | The card is invalid | | `card_address_mismatch` | Card billing address does not match | | `card_zip_mismatch` | Card billing ZIP code does not match | | `card_cvv_invalid` | The card CVV is invalid | | `card_expired` | The card has expired | | `card_limit_violated` | Card limit was exceeded | | `card_not_honored` | Card was not honored by the issuer | | `card_cvv_required` | Card CVV is required but was not provided | | `card_restricted` | Card use is restricted | | `card_account_ineligible` | Card account is not eligible for this transaction | | `card_network_unsupported` | The card network is not supported | | `channel_invalid` | The payment channel is invalid | | `unauthorized_transaction` | Transaction was unauthorized | | `bank_account_ineligible` | The bank account is not eligible for this transaction | | `bank_transaction_error` | There was an error processing the transaction on the bank | | `invalid_account_number` | The bank account number is invalid | | `invalid_wire_rtn` | The wire RTN is invalid | | `invalid_ach_rtn` | The ACH RTN is invalid | | `ref_id_invalid` | The reference ID is invalid | | `account_name_mismatch` | The name on the account does not match | | `account_number_mismatch` | The account number does not match | | `account_ineligible` | The account is not eligible for this transaction | | `wallet_address_mismatch` | The wallet address does not match | | `customer_name_mismatch` | The customer name does not match | | `institution_name_mismatch` | The institution name does not match | | `vendor_inactive` | The vendor is inactive | ### Payout error codes | Code | Description | | ----------------------------- | ------------------------------------------------------------------------------------------- | | `insufficient_funds` | Insufficient funds | | `transaction_denied` | Transaction was denied by Circle Risk Service, see payout `riskEvaluation` for more details | | `transaction_failed` | Transaction failed due to an unknown reason | | `transaction_returned` | Transaction was returned | | `bank_transaction_error` | There was an error processing the transaction on the bank | | `fiat_account_limit_exceeded` | The fiat account limit was exceeded | | `invalid_bank_account_number` | The bank account number is invalid | | `invalid_ach_rtn` | The ACH RTN is invalid | | `invalid_wire_rtn` | The wire RTN is invalid | | `vendor_inactive` | The vendor is inactive | ### Transfer error codes | Code | Description | | -------------------- | ---------------------------------------------------------------------------------------------- | | `transfer_failed` | The transfer failed due to unknown reasons | | `transfer_denied` | The transfer was denied by Circle Risk Service, see transfer `riskEvaluation` for more details | | `blockchain_error` | There was an error processing the transfer onchain | | `insufficient_funds` | There was not enough funding to cover the transfer amount | # Notifications Quickstart Source: https://developers.circle.com/circle-mint/circle-apis-notifications-quickstart Configure a notification subscriber endpoint and be notified when a payment is received. *Circle notifications will inform you every time the status of a resource changes, such as changes in payment status. Notifications can be accessed by setting up a notification subscriber endpoint on your end. This quickstart guide shows how to set up notifications for the Circle APIs. Follow this to configure a subscriber endpoint that sends a notification each time the resource status changes.* Note: This guide addresses the payment resource, but is applicable to any of the resources mentioned in [Circle API Notifications](/circle-mint/notifications-data-models). ## 1. Expose a Subscriber Endpoint To receive notifications on changes in resource status, you must expose a publicly accessible subscriber endpoint on your side. The endpoint must handle both `HEAD` and `POST` requests over HTTPS. To expose an endpoint for testing, you can use [webhook.site](https://webhook.site/) to inspect, test and automate incoming HTTPS requests or e-mails directly in the web browser. **💡 Tip:** When you visit [webhook.site](https://webhook.site/) for the first time, you should see a status message that looks similar to the following: `Your unique URL (Please copy it from here, not from the address bar!) https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7` Navigate to [webhook.site](https://webhook.site/) and record the value of the URL shown as `Your unique URL`: In the example above, the unique URL is:`https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7`. Use the public-facing URL you receive as you progress throughout this guide. ## 2. Subscribe to Status Notifications Now that you have a publicly accessible endpoint, you need to register your endpoint as a subscriber to webhook notifications by doing the following: 1. Navigate to **API > Subscriptions** in your Circle Mint account and click **Add Subscription**. 2. Enter your endpoint URL from above. It will be similar to the earlier example: `https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7`. 3. Click **Add Subscription**: 4. You should receive two responses on your local server shell that confirm the subscription with a body similar to the following: ```json JSON theme={null} { "Type": "SubscriptionConfirmation", "MessageId": "ddbdcdcf-d36a-45b5-927c-da25b9b009ae", "Token": "2336412f37fb687f5d51e6e2425f004aed7b7526d5fae41bc257a0d80532a6820258bf77eb25b90453b863450713a2a5a4250696d725a306ef39962b5b543752c9003e0841c0e61253fd6c517a94edebe44f36c5fe4ba131c8ea5f6f42a43f97f6e1865505e2f29f79a62f89e18f97e03a0dd5d982a7578c8d6e21154163f2d6aae523cff25557f9bc21b2503d413006", "TopicArn": "arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic", "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", "SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic&Token=2336412f37fb687f5d51e6e2425f004aed7b7526d5fae41bc257a0d80532a6820258bf77eb25b90453b863450713a2a5a4250696d725a306ef39962b5b543752c9003e0841c0e61253fd6c517a94edebe44f36c5fe4ba131c8ea5f6f42a43f97f6e1865505e2f29f79a62f89e18f97e03a0dd5d982a7578c8d6e21154163f2d6aae523cff25557f9bc21b2503d413006", "Timestamp": "2020-04-11T20:50:16.324Z", "SignatureVersion": "1", "Signature": "kBr9z/ysQrr0ldowHY4lThkOA+dwyjcsyx7NwkbTkgEKG4N61BSSEA+43aYQEB/Ml09hclybvyjyRKWYOjaxQgbUXWmyWrCQ7vY93WYhuGvOqZxAMPiDiILxLs6/KtOxneKVvzfpK4abLrYyTTA+z/dQ52h9L8eoiSKSW81e4clfYBTJkGmuAPKFC08FvEAVT89VikPp68mBf4CctPv3Em0b4J1VvDhAB21B2LekgUmwUE0aE7fUbsF3XsKGQd/fDshLOJasQEuXSqdB5X7LITBA8r24FY+wCjwm8oR3VI9IMy21fUC6wMgoFIVZHW1KxzpEkMCSe7R1ySdNIru8SQ==", "SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem" } ``` **Note:** The response body above is in a plain text format, so be sure you're not expecting a JSON content type. 5. To complete the subscription process, visit the `SubscribeURL` link in each response. Messages won't be sent to the endpoint until you confirm the subscription by accessing the URL. 6. After you visit the `SubscribeURL` links, the subscription status updates to `COMPLETE`, indicating it's ready for use. **Note:** In the Circle sandbox environment, if your subscription is stuck in the `PENDING` (unconfirmed) state, reach out to [customer-support@circle.com](mailto:customer-support@circle.com) to remove it. In the production environment, if your subscription is stuck in the `PENDING` state, it's automatically removed after 72 hours. You now have a sample local environment ready to receive notifications. # Circle SDKs Source: https://developers.circle.com/circle-mint/circle-sdks Use the TypeScript SDK to quickly get your codebase up and running with Circle APIs. Circle's SDKs help you integrate Circle APIs. The TypeScript SDK is open source. If you find a bug or need a feature, file an issue or contribute to the [`node-sdk`](https://github.com/circlefin/circle-nodejs-sdk). **Note:** This page contains code snippets and examples that can be run as is or incorporated into your apps. ## Installing the SDK Run one of the following commands from your project directory to install the SDK. ```shell Node theme={null} npm install @circle-fin/circle-sdk --save # or yarn add @circle-fin/circle-sdk ``` ## Configuration ```typescript theme={null} import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk"; const circle = new Circle( "", CircleEnvironments.sandbox, // API base url ); ``` ## List balances ```typescript theme={null} async function listBalances() { const balancesRes = await circle.balances.listBalances(); } ``` ## Create a crypto payment ```typescript theme={null} async function createCryptoPayment() { const createCryptoPaymentRes = await circle.paymentIntents.createPaymentIntent({ idempotencyKey: "5c6e9b91-6563-47ec-8c6d-0ce1103c50b3", amount: { amount: "3.14", currency: "USD", }, settlementCurrency: "USD", paymentMethods: [ { chain: "ETH", type: "blockchain", }, ], }); } ``` ## Get a crypto payment ```typescript theme={null} async function getCryptoPayment(id: string) { const cryptoPayment = await circle.paymentIntents.getPaymentIntent(id); } ``` # How Minting and Redemption Works Source: https://developers.circle.com/circle-mint/concepts/how-minting-works Understand how Circle Mint converts fiat to USDC (minting) and USDC back to fiat (redemption), including settlement timing, account structure, and compliance requirements. Circle Mint's core operations are minting and redemption. Minting converts fiat currency into stablecoins (USDC or EURC), and redemption converts stablecoins back to fiat. Every token mints and redeems at a 1:1 ratio with the underlying fiat currency. This page explains the mental model behind these operations, including settlement timing, account structure, onchain transfers, fees, and compliance. ## Minting Minting (also known as an onramp) is the process of depositing fiat currency and receiving an equivalent amount of stablecoins. When you send a fiat transfer to Circle, Circle credits your Mint account balance with the corresponding stablecoin amount at a 1:1 ratio. The minting flow works as follows: 1. You initiate a fiat transfer from your linked bank account to Circle. 2. Circle receives the fiat deposit and credits your Mint account balance. 3. The stablecoins become available for onchain transfers or other operations. Circle supports multiple payment rails for fiat deposits, including standard wires (FedWire and SWIFT), real-time interbank rails (RTP, SPEI, SEPA, and CHATS) in supported regions, and book transfers when you bank with one of Circle's settlement partners. Rail availability depends on your region and the currency you are depositing. **Settlement timing:** Domestic wire deposits received before the daily cutoff settle on the same business day. Real-time interbank rails settle in seconds, subject to network operating hours and transaction limits. International wires take 1-3 business days depending on intermediary banks. In the sandbox environment, mock wire deposits process in batches and may take up to 15 minutes. For step-by-step instructions, see [Deposit Fiat](/circle-mint/howtos/deposit-fiat). ## Redemption Redemption (also known as an offramp) is the reverse of minting. You convert stablecoins back to fiat currency by creating a payout to a linked bank account. The redemption flow works as follows: 1. You create a payout request specifying the amount and destination bank account. 2. Circle debits the stablecoin amount from your Mint account balance. 3. Circle sends a fiat transfer to your bank account using the appropriate rail for your region and bank. **Settlement timing:** Payouts typically settle on the next business day. In some cases, your bank may reject the incoming wire, resulting in a returned withdrawal. If a payout is returned, the funds are credited back to your Mint account balance. For step-by-step instructions, see [Withdraw Fiat](/circle-mint/howtos/withdraw-fiat). ## Account structure Your Circle Mint account has several key components that work together to support minting, redemption, and onchain transfers. ### Primary wallet Every Circle Mint account has a primary wallet identified by a `masterWalletId`. You retrieve this identifier from the `/v1/configuration` endpoint. The primary wallet serves as the source for outbound transfers and the destination for inbound deposits. ### Balances Your account maintains two types of balances: * **Available balance:** Settled funds you can transfer or redeem immediately. * **Unsettled balance:** Funds that are in transit and not yet available. Wire deposits appear as unsettled until they clear. ### Linked bank accounts You register external bank accounts to send and receive fiat. Each linked bank account receives a unique Virtual Account Number (VAN). When you wire funds to Circle using the VAN, Circle attributes the deposit to your account without requiring a tracking reference in the payment instruction. ### Deposit addresses Circle generates one deposit address per blockchain for your account. These addresses receive inbound stablecoin transfers from external wallets. You retrieve your deposit addresses through the API for each [supported blockchain](/circle-mint/references/supported-chains-and-currencies). ### Recipient addresses Recipient addresses are external blockchain addresses that you register and allowlist for outbound transfers. You must create a recipient address before you can send stablecoins to it. This allowlisting step provides an additional layer of security for outbound transfers. ## Onchain transfers Circle Mint supports both receiving and sending stablecoins onchain. * **Receiving:** External wallets send USDC or EURC to your deposit address on any supported blockchain. Circle detects the transfer and credits your account after the required number of [blockchain confirmations](/circle-mint/references/blockchain-confirmations). * **Sending:** You create a transfer to a registered recipient address. Circle debits your balance and broadcasts the transaction onchain. ### Transfer status lifecycle Onchain transfers progress through the following statuses: | Status | Description | | ---------- | ------------------------------------------------------------------- | | `pending` | The transfer request is created but not yet broadcast onchain. | | `running` | The transaction is broadcast and awaiting blockchain confirmations. | | `complete` | The required confirmations are reached and the transfer is final. | For details on confirmation requirements per blockchain, see [Blockchain Confirmations](/circle-mint/references/blockchain-confirmations). For step-by-step transfer instructions, see [Transfer USDC Onchain](/circle-mint/howtos/transfer-on-chain). ## Network fees Circle covers gas fees for outbound stablecoin transfers in most cases. You do not need to hold the native token of each blockchain to send USDC or EURC from your Mint account. ## Travel Rule compliance Transfers of \$3,000 or more in value on supported blockchains are subject to the FinCEN Travel Rule, which requires identity data about the originator of the transaction. How Circle handles the identity requirement depends on the type of transfer: * **Business account transfers:** Circle uses your company's identity stored on file. You do not need to include identity data in each request. * **Third-party payouts:** If you send funds on behalf of someone else, you must provide the originator's identity (name and address) in the payout request. Omitting required identity data causes the transfer to fail. For the full list of supported blockchains and implementation details, see [Travel Rule compliance](/circle-mint/howtos/transfer-on-chain#travel-rule-compliance). ## Approval workflows Customers in France and Singapore are subject to additional recipient address verification requirements. Before an outbound transfer can proceed, the recipient address must be verified through the [Mint Console](https://app.circle.com/signin). This approval step ensures compliance with local regulatory requirements in those jurisdictions. # Credit API Source: https://developers.circle.com/circle-mint/credit-api/overview Programmatically manage Settlement Advance and Line of Credit products through your Circle Mint account. The Credit API provides programmatic access to Circle Mint's credit products. You can initiate draws, track transfer status, manage fees, and process repayments through a dedicated set of endpoints under `/v1/credit/`. The Credit API is available to institutional Circle Mint customers with an approved credit facility. A separate offline credit contract through Circle is required before you can access these endpoints. [Contact Circle](https://www.circle.com/mint-contact) to learn more about credit products. ## Credit products Bridges settlement timing gaps through a reserve-then-request flow. You reserve funds, upload wire proof, and wait for Treasury approval before disbursement. Provides single-request draws with auto-approval and asynchronous disbursement. No wire proof required. Supports crypto repayment directly from your Circle Mint wallet. ## Key concepts * **Credit line**: Your facility object, including credit limit, used and available amounts, fee rates, minimum balance requirement, and validation errors. Each customer has one credit line. * **Credit transfers**: Individual draw requests against your credit line. Settlement Advance transfers progress through `funds_reserved`, `requested`, `disbursed`, and `paid`. Line of Credit transfers progress through `requested`, `disbursed`, and `paid`. Both can reach `past_due` if unpaid after the due date. Transfers on daily cadence have a 7-day repayment due date from disbursement, and transfers on hourly cadence have a 24-hour repayment due date from disbursement. * **Fees**: Each draw can include two types of fees: a draw fee (Settlement Advance only) charged at disbursement and a recurring fee accrued on a cadence determined by your credit line's `feeCadence` setting: daily (every 24 hours) or hourly (every hour). The `recurringFee` rate in `feeRates` applies at whichever cadence your credit line uses. Hourly cadence is available only on Line of Credit. Fees are auto-deducted from your Circle Mint wallet balance. * **Minimum balance**: A required USDC balance (`minBalance`) that you must maintain in your Circle Mint wallet while you have an active credit line. * **Validation errors**: Conditions that block new transfer creation, such as `INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`. Returned in the credit line response so you can resolve them before initiating a draw. * **Blockchain destination**: An optional feature that sends disbursed funds directly to a verified blockchain address instead of your Circle Mint wallet. Include the `destination` field when creating a transfer, referencing an address from your Circle Mint recipient address book. If omitted, funds are deposited into your Circle Mint wallet by default. To manage verified recipient addresses, use the [recipient address endpoints](/api-reference/circle-mint/account/create-business-recipient-address). ## Get started Reserve funds, upload wire proof, and track your advance through disbursement Request a draw, monitor its status, and repay using crypto # Quickstart: Line of Credit Source: https://developers.circle.com/circle-mint/credit-api/quickstart-line-of-credit Request a draw, track disbursement, and repay outstanding balances using the Credit API's Line of Credit endpoints. This quickstart walks you through a complete Line of Credit draw and repayment lifecycle using the Credit API. You check your available credit, request a draw, track the transfer through disbursement, and repay using USDC from your Circle Mint wallet. ## Prerequisites Before you begin, ensure you have: * An approved Line of Credit facility with Circle. Contact your Circle representative if you haven't completed the offline credit agreement. * A Circle Mint API key with the Credit API entitlement enabled. * Familiarity with [Circle Mint authentication](/circle-mint/quickstarts/getting-started) and [idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests). ## Step 1: Check your credit line status Before requesting a draw, verify that your credit line is active and has sufficient available credit. Call the `GET /v1/credit` endpoint to retrieve your credit line details. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json Response theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "product": "lineOfCredit", "feeCadence": "daily", "status": "active", "limit": { "amount": "1000000.00", "currency": "USD" }, "used": { "amount": "250000.00", "currency": "USD" }, "available": { "amount": "750000.00", "currency": "USD" }, "outstandingTransfers": 2, "feeRates": { "recurringFee": "0.0003" }, "unpaidFees": { "amount": "150.00", "currency": "USD" }, "minBalance": { "amount": "100000.00", "currency": "USD" }, "validationErrors": [], "createDate": "2024-01-15T10:30:00.000Z", "updateDate": "2024-03-20T14:22:00.000Z" } } ``` Confirm the following before proceeding: * `product` is `lineOfCredit`. * `status` is `active`. * `available.amount` is greater than or equal to the amount you plan to draw. * `validationErrors` is an empty array. If it contains errors such as `INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`, resolve them before you create a new transfer. The `feeCadence` field indicates how frequently fees accrue on your credit line. The `recurringFee` rate in `feeRates` applies at whichever cadence your credit line uses. * **Daily** (every 24 hours): Available on both products. Transfers have a 7-day due date from disbursement. * **Hourly** (every hour): Available only on Line of Credit. Transfers have a 24-hour due date from disbursement. For an hourly cadence credit line, the relevant fields differ as follows: ```json Response (hourly cadence — other fields unchanged) theme={null} { "data": { "feeCadence": "hourly", "feeRates": { "recurringFee": "0.0000125" } } } ``` ## Step 2: Request a draw Request a draw against your credit line by calling `POST /v1/credit/transfers` with an idempotency key and the desired amount. Unlike Settlement Advance transfers, LoC draws are auto-approved and don't require wire proof or Treasury review. ```bash theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/transfers' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "amount": { "amount": "50000.00", "currency": "USD" }, "destination": { "type": "verified_blockchain", "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } }' ``` The `destination` field is optional. If included, disbursed funds are sent to the specified verified blockchain address instead of your Circle Mint wallet. The `addressId` must reference a verified address from your [Circle Mint recipient address book](/api-reference/circle-mint/account/create-business-recipient-address). If you omit `destination`, funds are deposited into your Circle Mint wallet by default. ```json Response theme={null} { "data": { "id": "a1c2e3f4-5678-4d90-b123-456789abcdef", "amount": { "amount": "50000.00", "currency": "USD" }, "status": "requested", "blockchainDestination": { "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "status": "pending" }, "createDate": "2024-03-20T14:20:00.000Z", "updateDate": "2024-03-20T14:20:00.000Z" } } ``` Save the transfer `id` from the response. You need it to track the transfer in the next step. The transfer starts in `requested` status and automatically transitions to `disbursed` once the funds are deposited into your Circle Mint wallet, or sent to the verified blockchain address if you specified a `destination`. No manual approval step is required. ## Step 3: Track the transfer Monitor your transfer as it progresses from `requested` to `disbursed`. You can poll the transfer endpoint or subscribe to webhook notifications. ### Poll the transfer endpoint Call `GET /v1/credit/transfers/{id}` to check the current transfer status. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/transfers/a1c2e3f4-5678-4d90-b123-456789abcdef' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` Once the transfer is disbursed, the response includes fee and repayment details: ```json Response theme={null} { "data": { "id": "a1c2e3f4-5678-4d90-b123-456789abcdef", "amount": { "amount": "50000.00", "currency": "USD" }, "status": "disbursed", "outstanding": { "amount": "50150.00", "currency": "USD" }, "fees": { "total": { "amount": "150.00", "currency": "USD" }, "unpaid": { "amount": "150.00", "currency": "USD" } }, "dueDate": "2024-03-27T14:22:00.000Z", "disbursedDate": "2024-03-20T14:22:00.000Z", "blockchainDestination": { "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "status": "complete", "transferId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321" }, "createDate": "2024-03-20T14:20:00.000Z", "updateDate": "2024-03-21T00:00:00.000Z" } } ``` The `outstanding` amount includes the original draw plus any accrued fees. The `dueDate` indicates when repayment is expected. For lines on daily cadence, the due date is 7 days from disbursement. For lines on hourly cadence, the due date is 24 hours from disbursement. If you included a `destination` in your request, the `blockchainDestination` field tracks the status of the onchain transfer. | Status | Description | `transferId` populated | | ----------- | ------------------------------------------- | ---------------------- | | `pending` | Destination recorded, awaiting disbursement | No | | `initiated` | Blockchain transfer submitted | Yes | | `complete` | Funds arrived at destination address | Yes | | `failed` | Blockchain transfer failed | Yes | ### Subscribe to webhook notifications For real-time updates, subscribe to webhook notifications through the Circle Mint webhook management UI: * **`creditTransfers`**: Receive notifications whenever a transfer changes status, such as the transition from `requested` to `disbursed`. * **`creditFees`**: Receive notifications when fees are accrued against your outstanding transfers. * **`creditRepayments`**: Receive notifications when a repayment is received and allocated against your outstanding balance. The Line of Credit transfer follows this status lifecycle: ```mermaid theme={null} stateDiagram-v2 [*] --> requested requested --> disbursed requested --> rejected disbursed --> paid disbursed --> past_due past_due --> paid ``` A transfer can also reach `rejected` (if the request is declined) or `past_due` (if repayment is overdue after the due date). ## Step 4: Repay outstanding balances You can repay Line of Credit draws in two ways: * **Crypto repayment**: Deduct USDC directly from your Circle Mint wallet balance using the `POST /v1/credit/cryptoRepayment` endpoint. * **Wire repayment**: Send a wire transfer to Circle using the account details from `GET /v1/credit/repaymentAccounts/{fiatAccountId}`. You need a `fiatAccountId` from your wire bank accounts (see [list wire bank accounts](/api-reference/circle-mint/account/list-business-wire-accounts)). Include the `trackingRef` value on your wire so Circle can match the payment to your credit line. For details on retrieving repayment account details, see the [Settlement Advance quickstart](/circle-mint/credit-api/quickstart-settlement-advance#step-2-get-repayment-account-details). The following example shows a crypto repayment. Call `POST /v1/credit/cryptoRepayment` with the amount you want to apply toward your outstanding balance. ```bash theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/cryptoRepayment' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotencyKey": "d7a3e2c1-8f45-4b91-ae67-2c9d0f8b3e5a", "amount": { "amount": "25000.00", "currency": "USD" } }' ``` ```json Response theme={null} { "data": { "id": "e5f6a7b8-9012-3456-efab-345678901234", "amount": { "amount": "25000.00", "currency": "USD" }, "status": "pending", "createDate": "2024-03-25T10:00:00.000Z", "updateDate": "2024-03-25T10:00:00.000Z" } } ``` The requested repayment amount is capped at your total outstanding balance. If you specify an amount greater than what you owe, the API applies only the outstanding balance. Crypto repayment is available exclusively for Line of Credit products. ## Step 5: Verify repayment status After initiating a repayment, verify that it completes successfully. You can poll the repayments endpoint or subscribe to webhook notifications. ### Poll the repayments endpoint Call `GET /v1/credit/repayments` to list all repayments and check their status. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/repayments' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` Once the repayment is processed, the response shows the completed payment with the amount applied: ```json Response theme={null} { "data": [ { "id": "d4e5f6a7-b890-1234-defa-234567890123", "transferId": "a1c2e3f4-5678-4d90-b123-456789abcdef", "amountApplied": { "amount": "25000.00", "currency": "USD" }, "paymentAmount": { "amount": "25000.00", "currency": "USD" }, "type": "crypto", "status": "completed", "settlementDate": "2024-04-08T12:00:00.000Z", "createDate": "2024-04-08T12:00:00.000Z", "updateDate": "2024-04-08T12:00:00.000Z" } ] } ``` The `transferId` field links the repayment to the original draw. The `amountApplied` reflects the portion of the payment allocated to that transfer's outstanding balance. ### Subscribe to webhook notifications For real-time repayment tracking, subscribe to `creditRepayments` webhook notifications through the Circle Mint webhook management UI. You receive notifications when a repayment is received and allocated against your outstanding transfers. # Quickstart: Settlement Advance Source: https://developers.circle.com/circle-mint/credit-api/quickstart-settlement-advance Reserve funds, upload wire proof, and track your Settlement Advance through disbursement using the Credit API. This quickstart walks you through a complete Settlement Advance draw lifecycle using the Credit API. You check your available credit, reserve funds, upload wire proof for Treasury review, and track the transfer through to disbursement. ## Prerequisites Before you begin, ensure you have: * An approved Settlement Advance credit facility with Circle. Contact your Circle representative if you haven't completed the offline credit agreement. * A Circle Mint API key with the Credit API entitlement enabled. * Familiarity with [Circle Mint authentication](/circle-mint/quickstarts/getting-started) and [idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests). ## Step 1: Check your credit line status Before initiating a draw, verify that your credit line is active and has sufficient available credit. Call the `GET /v1/credit` endpoint to retrieve your credit line details. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json Response theme={null} { "data": { "id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32", "product": "settlementAdvance", "feeCadence": "daily", "status": "active", "limit": { "amount": "500000.00", "currency": "USD" }, "used": { "amount": "100000.00", "currency": "USD" }, "available": { "amount": "400000.00", "currency": "USD" }, "outstandingTransfers": 1, "feeRates": { "drawFee": "0.0001", "recurringFee": "0.0003" }, "unpaidFees": { "amount": "0.00", "currency": "USD" }, "minBalance": { "amount": "50000.00", "currency": "USD" }, "validationErrors": [], "createDate": "2024-01-15T10:30:00.000Z", "updateDate": "2024-03-20T14:22:00.000Z" } } ``` Confirm the following before proceeding: * `status` is `active`. * `available.amount` is greater than or equal to the amount you plan to reserve. * `validationErrors` is an empty array. If it contains errors such as `INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`, resolve them before you create a new transfer. The `feeCadence` field indicates how frequently fees accrue on your credit line. Settlement Advance credit lines always use `daily` cadence. ## Step 2: Get repayment account details Before initiating a draw, retrieve the repayment account details for your wire bank account. You need the beneficiary information and tracking reference to send your wire. First, identify the `fiatAccountId` of the wire bank account you want to use for repayment. You can list your existing wire bank accounts with `GET /v1/businessAccount/banks/wires` (see the [list wire bank accounts](/api-reference/circle-mint/account/list-business-wire-accounts) API reference). Once you have the `fiatAccountId`, call `GET /v1/credit/repaymentAccounts/{fiatAccountId}` to get the beneficiary details and tracking reference for your wire. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/repaymentAccounts/b8627ae8-732b-4d25-b947-1df8f4007a29' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json Response theme={null} { "data": { "id": "c9f2a1b3-6d84-4e0f-b512-9a8c7e3d4f01", "description": "WELLS FARGO BANK, NA ****1111", "status": "unverified", "wireInstructions": { "trackingRef": "CIR3XBZZ4N", "beneficiary": { "name": "CIRCLE INTERNET FINANCIAL INC", "address1": "1 Main Street", "address2": "Suite 1" }, "beneficiaryBank": { "swiftCode": "SVBKUS6S", "routingNumber": "121140399", "accountNumber": "3302726104", "currency": "USD", "name": "SILICON VALLEY BANK", "address": "3003 TASMAN DRIVE", "city": "SANTA CLARA", "postalCode": "95054", "country": "US" } } } } ``` Use the `trackingRef` value (at `data.wireInstructions.trackingRef`) as the reference on your wire transfer so that Circle can match the incoming payment to your credit line. Save the beneficiary bank details for use when initiating your wire. A `status` of `unverified` is expected for first-time use. The status becomes `active` after a completed credit repayment is matched to this account. **Sandbox only:** You can simulate an incoming wire payment without sending real funds by calling the mock repayment endpoint: ```bash theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/mocks/repayments' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amount": { "amount": "100000.00", "currency": "USD" } }' ``` This creates a simulated wire arrival and auto-links the fiat account as a repayment account if it is not already linked. ## Step 3: Reserve funds The reserve step holds the requested amount for you while you initiate and confirm your wire transfer. Reserve funds against your credit line by calling `POST /v1/credit/transfers/reserveFunds`. This creates a transfer in `funds_reserved` status and holds the requested amount against your available credit. ```bash theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/transfers/reserveFunds' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "amount": { "amount": "100000.00", "currency": "USD" }, "destination": { "type": "verified_blockchain", "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } }' ``` The `destination` field is optional. If included, disbursed funds are sent to the specified verified blockchain address instead of your Circle Mint wallet. The `addressId` must reference a verified address from your [Circle Mint recipient address book](/api-reference/circle-mint/account/create-business-recipient-address). If you omit `destination`, funds deposit into your Circle Mint wallet by default. ```json Response theme={null} { "data": { "id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "funds_reserved", "expiresAt": "2024-03-20T15:00:00.000Z", "blockchainDestination": { "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "status": "pending" }, "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-20T14:30:00.000Z" } } ``` Save the transfer `id` from the response. You need it for the remaining steps. Reserved funds expire after 30 minutes. If you don't upload wire proof before the `expiresAt` timestamp, the reservation automatically transitions to `expired` status and the funds return to your available credit. Only one transfer can be in `funds_reserved` status per credit line at a time. You must complete or cancel the current reservation before creating a new one. ## Step 4: Upload wire proof Using the beneficiary details and tracking reference from Step 2 (found at `data.wireInstructions.trackingRef` in the response), initiate your wire transfer through your bank. Then upload proof of the wire to request disbursement. Call `PUT /v1/credit/transfers/{id}/requestReservedFunds` with a `multipart/form-data` request containing your wire proof document. Accepted file types are PDF (`application/pdf`), JPEG (`image/jpeg`), and PNG (`image/png`). ```bash theme={null} curl --location --request PUT 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32/requestReservedFunds' \ --header 'Authorization: Bearer YOUR_API_KEY' \ --form 'fileName="wire-confirmation.pdf"' \ --form 'fileContent=@"/path/to/wire-confirmation.pdf"' ``` ```json Response theme={null} { "data": { "id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "requested", "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-20T14:45:00.000Z" } } ``` The transfer status transitions from `funds_reserved` to `requested`. At this point, Circle's Treasury team reviews the wire proof. No further action is required from you until the review completes. ## Step 5: Track the transfer status Monitor your transfer as it progresses through the review and disbursement process. You can poll the transfer endpoint or subscribe to webhook notifications. ### Poll the transfer endpoint Call `GET /v1/credit/transfers/{id}` to check the current transfer status. ```bash theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` Once Treasury approves the transfer and disburses funds, the response reflects the `disbursed` status along with fee and repayment details: ```json Response theme={null} { "data": { "id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "disbursed", "outstanding": { "amount": "102000.00", "currency": "USD" }, "fees": { "total": { "amount": "2000.00", "currency": "USD" }, "unpaid": { "amount": "2000.00", "currency": "USD" } }, "dueDate": "2024-03-28T10:00:00.000Z", "disbursedDate": "2024-03-21T10:00:00.000Z", "blockchainDestination": { "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "status": "complete", "transferId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321" }, "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-21T10:00:00.000Z" } } ``` The `outstanding` amount includes the original draw plus any accrued fees. The `dueDate` indicates when repayment is expected. Settlement Advance transfers use daily cadence with a 7-day repayment due date from disbursement. If you included a `destination` in your request, the `blockchainDestination` field tracks the status of the onchain transfer. | Status | Description | `transferId` populated | | ----------- | ------------------------------------------- | ---------------------- | | `pending` | Destination recorded, awaiting disbursement | No | | `initiated` | Blockchain transfer submitted | Yes | | `complete` | Funds arrived at destination address | Yes | | `failed` | Blockchain transfer failed | Yes | ### Subscribe to webhook notifications For real-time updates, subscribe to webhook notifications through the Circle Mint webhook management UI: * **`creditTransfers`**: Receive notifications whenever a transfer changes status, such as the transition from `requested` to `disbursed`. * **`creditFees`**: Receive notifications when fees are accrued against your outstanding transfers. * **`creditRepayments`**: Receive notifications when a repayment is received and allocated against your outstanding balance. The Settlement Advance transfer follows this status lifecycle: ```mermaid theme={null} stateDiagram-v2 [*] --> funds_reserved funds_reserved --> requested funds_reserved --> expired funds_reserved --> canceled requested --> disbursed requested --> rejected disbursed --> paid disbursed --> past_due past_due --> paid ``` A transfer can also reach `expired` (if the reservation times out), `rejected` (if Treasury declines the request), or `past_due` (if repayment is overdue). In sandbox, Settlement Advance requests are auto-approved. To simulate a rejection, reserve funds with an amount of `119.53`. ## Cancel a reservation (optional) If you need to cancel a reservation before uploading wire proof, call `PUT /v1/credit/transfers/{id}/cancelReserve`. You can only cancel a transfer that is in `funds_reserved` status. ```bash theme={null} curl --location --request PUT 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32/cancelReserve' \ --header 'Authorization: Bearer YOUR_API_KEY' ``` ```json Response theme={null} { "data": { "id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "canceled", "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-20T14:50:00.000Z" } } ``` After cancellation, the reserved amount returns to your available credit and you can create a new reservation. # Crypto Payouts Payment Reason Codes Source: https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes Payment reason codes for the Crypto Payouts API purposeOfTransfer field. Use a `purposeOfTransfer` value when you [create a crypto payout](/api-reference/circle-mint/payouts/create-payout), if your entity configuration requires it. Each value is a payment reason code. The table lists codes accepted for Crypto Payouts. Values align with [CPN payment reason codes](/cpn/references/errors/payment-reason-codes). This list adds `PMT000` for cases that do not match another code. Do not use `PMT006`; it is not valid for crypto payouts. | Reason code | Description | | ----------- | ------------------------------------------------------------------------------------------------------ | | `PMT000` | Others | | `PMT001` | Invoice payment | | `PMT002` | Payment for services | | `PMT003` | Payment for software | | `PMT004` | Payment for imported goods | | `PMT005` | Travel services | | `PMT007` | Repayment of loans | | `PMT008` | Payroll | | `PMT009` | Payment of property rental | | `PMT010` | Information service charges | | `PMT011` | Advertising and public relations related expenses | | `PMT012` | Royalty fees, trademark fees, patent fees, and copyright fees | | `PMT013` | Fees for brokers, front end fee, commitment fee, guarantee fee, and custodian fee | | `PMT014` | Fees for advisors, technical assistance, and academic knowledge including remuneration for specialists | | `PMT015` | Representative office expenses | | `PMT016` | Tax payment | | `PMT017` | Transportation fees for goods | | `PMT018` | Construction costs/expenses | | `PMT019` | Insurance premium | | `PMT020` | General goods trades (offline) | | `PMT021` | Insurance claims payment | | `PMT022` | Remittance payments to friends or family | | `PMT023` | Education-related student expenses | | `PMT024` | Medical treatment | | `PMT025` | Donations | | `PMT026` | Mutual fund investment | | `PMT027` | Currency exchange | | `PMT028` | Advance payments for goods | | `PMT029` | Merchant settlement | | `PMT030` | Repatriation fund settlement | # Quickstart: Exchange Local Currency for USDC Source: https://developers.circle.com/circle-mint/exchange-local-currency-usdc The Cross-Currency API lets you swap local currency for USDC. To onramp funds in this way, you must first have a fiat account linked to your Circle Mint account. This guide walks you through how to use the Cross-Currency API to obtain a quote for a local currency to USDC exchange and then execute the swap. ## Prerequisites Before you begin this quickstart, ensure that you have: * Obtained an API key for Mint from Circle * Obtained access to the Cross-Currency API * Installed cURL on your development machine * Linked a fiat account to your Circle Mint account This quickstart provides API requests in cURL format, along with example responses. ## Part 1: Request a quote To begin an onramp from fiat to USDC, you must first request a quote. Quotes have a rate guarantee until they expire. **Note:** [Step 1.1.](#11-get-a-list-of-linked-fiat-accounts) and [Step 1.2.](#12-specify-which-fiat-account-to-use) only need to be performed once. If you want to switch the fiat account attached to your FX trading account, you can repeat these two steps. ### 1.1. Get a list of linked fiat accounts Retrieve a list of the fiat accounts linked to your Circle Mint account using the following request: ```shell Shell theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/businessAccount/banks/pix' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` **Response** ```json JSON theme={null} { "data": [ { "id": "763a80d8-9bbb-4876-a67d-e8089389b016", "type": "wire", "status": "complete", "createDate": "2025-09-16T15:53:47.251Z", "updateDate": "2025-09-16T15:53:48.293Z", "description": "Banco Azteca ****7771", "trackingRef": "CIR3A3R7EV", "virtualAccountEnabled": true, "fingerprint": "faf6b92c-4e76-426c-9741-95aed1415715", "billingDetails": { "name": "Satoshi Nakamoto", "line1": "100 Money Street", "line2": "Suite 1", "city": "Boston", "postalCode": "01234", "district": "MA", "country": "US", "valid": true }, "bankAddress": { "bankName": "Banco Azteca", "line1": "Iabel la Católica 165", "line2": "Colonia Obrera", "city": "México DF", "district": "México DF", "country": "MX" } } ] } ``` ### 1.2. Specify which fiat account to use Register your local fiat account with the Cross-Currency API to make it the account that settles trades. The following is an example request to register your fiat account: ```shell Shell theme={null} curl --location --request PUT 'https://api-sandbox.circle.com/v1/exchange/fxConfigs/accounts' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016", "currency": "MXN" }' ``` **Response** ```json JSON theme={null} { "data": { "currency": "MXN", "fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016", "createDate": "2025-01-09T17:50:09.452241Z", "updateDate": "2025-02-28T17:25:15.866900Z" } } ``` ### 1.3. Request a quote from the API Using a UUIDv4 generator, generate a UUID to use as the idempotency key. Using the idempotency key, generate a quote for exchanging a specific amount of local currency to USDC. You must include an `amount` field on either the `from` or the `to` object, but not both. The `type` field must be set to `tradable` to get a locked rate quote. The following is an example request for a quote: **Note:** Quotes are valid for 3 seconds and must be refreshed if not used in that time frame. ```shell Shell theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/quotes' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "tradable", "idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b", "from": { "currency": "MXN", "amount": 100.00 }, "to": { "currency": "USDC", "amount": null } } ' ``` **Response** ```json JSON theme={null} { "data": { "id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1", "rate": 0.0597, "from": { "currency": "MXN", "amount": 100.0 }, "to": { "currency": "USDC", "amount": 5.01 }, "expiry": "2023-10-26T14:37:20.804786Z", "type": "tradable" } } ``` ## Part 2: Initiate the trade Once you have obtained a quote, you can lock in the rate by accepting it and initiating the trade. If you were building a UI for your users, you would display the quote to your users and let them confirm the trade. ### 2.1. Create the trade Generate another idempotency key, and then use it to confirm the quote and lock in the rate using the following example request: ```shell Shell theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/trades' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } ' ``` **Response** ```json JSON theme={null} { "data": { "id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "from": { "currency": "MXN", "amount": 100 }, "to": { "currency": "USDC", "amount": 5.01 }, "status": "pending", "createDate": "2023-10-26T14:37:20.804786Z", "updateDate": "2023-10-26T14:37:20.804786Z", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } } ``` ### 2.2. Check the trade status After creating a trade, you must check the trade status before sending funds. Use the following request to retrieve the trade status: ```shell Shell theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` **Response** ```json JSON theme={null} { "data": { "id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "from": { "currency": "MXN", "amount": 100 }, "to": { "currency": "USDC", "amount": 5.01 }, "status": "confirmed", "createDate": "2023-10-26T14:37:20.804786Z", "updateDate": "2023-10-26T14:37:21.123456Z", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } } ``` Only proceed to send funds when the trade status is `confirmed`. The possible status values are: * `pending`: The trade is not yet executed. Do not send funds. * `confirmed`: The trade is fully executed. You can proceed to send funds. * `failed`: The trade has failed. Do not send funds. ## Part 3: Send the funds Once you have confirmed that your trade status is `confirmed`, you can send the fiat funds to Circle. Do not send funds if the trade status is `pending` or `failed`. Once received, the USDC is transferred to the appropriate address. ### 3.1. Get settlement batches Retrieve unsettled payment batches from the API using the following example request: ```shell Shell theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` **Response** ```json JSON theme={null} { "data": [ { "id": "67276b7d-7ea5-4f22-a231-09e2d2891c36", "entityId": "c5692eb6-33f9-431d-9481-2eee38f02081", "status": "pending", "createDate": "2025-09-11T15:45:04.729442Z", "updateDate": "2025-09-11T15:45:04.729443Z", "details": [ { "id": "02bd22dc-b40f-49b8-b2d8-6e69f82cfca0", "type": "payable", "status": "pending", "reference": "ezBrwN2nP5Bz18Lu", "amount": { "currency": "MXN", "amount": "100.00" }, "expectedPaymentDueAt": "2025-09-11T16:45:00Z", "createDate": "2025-09-11T15:45:04.728466Z", "updateDate": "2025-09-11T15:45:04.728466Z" }, { "id": "afbd8d53-34ea-42fa-9691-4a5c0dd96c53", "type": "receivable", "status": "pending", "amount": { "currency": "USDC", "amount": "5.01" }, "createDate": "2025-09-11T15:45:04.728479Z", "updateDate": "2025-09-11T15:45:04.728479Z" } ] } ] } ``` ### 3.2. Get payment instructions Retrieve the settlement instructions for the currency you are using with the following example request. Note that this step only needs to be performed once, as the values are static and can be safely cached. ```shell Shell theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements/instructions/MXN' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` **Response** ```json JSON theme={null} { "data": { "currency": "MXN", "fiatAccountType": "wire", "instruction": { "beneficiary": { "name": "PLATAFORMA INTEGRAL DE CRIPTOS Y OTROS A", "address1": "AVENIDA INSURGENTES SUR 3579", "address2": "COLONIA VILLA OLÍMPICA, ALCALDÍA TLALPAN" }, "beneficiaryBank": { "swiftCode": "AZTKMXMMXXX", "routingNumber": "322286803", "accountNumber": "127180987654321012", "currency": "MXN", "name": "BANCO AZTECA", "address": "AVENIDA INSURGENTES SUR 3579", "postalCode": "14020", "country": "MX" }, "trackingRef": "ezBrwN2nP5Bz18Lu" } } } ``` ### 3.3. Create a fiat transfer Initiate an inbound fiat transfer to the account specified in the instructions. You should include the reference to the settlement batch in the transfer request. When Circle receives the transfer, Circle updates the settlement batch and transfers USDC to your Mint account. If you were building a UI for your users, you would display instructions on how to complete the transfer. In such a scenario, when the USDC is allocated to your Mint account, you must then transfer it to the end user that requested the trade. **Warning:** You must only send the fiat transfer on the specified payment rail. In this example, for `MXN`, you must send the transfer on the SPEI payment rail. Transfers on unsupported payment rails may take significant time and cost to recover. # How Stablecoin Payins and Payouts Work Source: https://developers.circle.com/circle-mint/how-stablecoin-payins-and-payouts-work Learn how stablecoin payins and payouts work, including payment intents, payin and payout flows, and refunds. Stablecoin payins and payouts let you move USDC and EURC onchain through Circle Mint. A stablecoin payin is when your customer sends USDC or EURC to you onchain and Circle records that transfer. A stablecoin payout is when you send USDC or EURC to a customer, vendor, or supplier onchain. ## Payment intents You create a payment intent when a customer chooses to pay with USDC or EURC on a [supported blockchain](/circle-mint/references/supported-chains-and-currencies). The intent carries settlement currency, allowed chains, and the deposit address your customer uses. ### Continuous and transient modes In practice, think of the two payment intent modes like this: * **Continuous** fits repeat deposits. The customer gets a long-lived deposit address they can use more than once (for example top-ups or billing that reuses one address). Each successful inbound transfer is recorded as its own payment. * **Transient** fits a one-time checkout. You declare a fixed amount for one purchase or session (for example one cart or invoice). You still create a payment intent and show a deposit address, but this address cannot be reused for later top-ups or future orders. The [Create payment intent](/api-reference/circle-mint/payments/create-payment-intent) endpoint reflects that split in the request and response. Continuous omits a top-level `amount` at creation. Transient requires `type: "transient"` and an `amount`. The following table compares what you send when you create an intent and how you should use the deposit address in each mode. | | Continuous | Transient | | ------------------ | ---------------------------------------------------- | ------------------------------------------------ | | Amount at creation | Omitted. You set `currency` and settlement. | You set a fixed `amount`. | | Deposit address | Same address can receive multiple transfers. | Suited to a single checkout or one-time payment. | | How you select it | Omit `type` (default) or set `type` to `continuous`. | Set `type` to `transient`. | When a transfer settles, Circle creates a [payment](/api-reference/circle-mint/payments/get-payment) for it. For continuous payment intents, each transfer is represented as its own payment. For transient payment intents, the deposit address is used for a single checkout or one-time payment. ## Payin flow To [receive a stablecoin payin](/circle-mint/receive-stablecoin-payin), the customer, your customer-facing UI, your server, and Circle follow the same steps whether the intent is continuous or transient. The customer-facing UI is the interface your customer uses to make a payment, such as your website or mobile app. Your server calls Circle Mint APIs and handles webhooks or polling. On checkout, the customer chooses to pay with stablecoins in your customer-facing UI. Your server calls the [Create a payment intent](/api-reference/circle-mint/payments/create-payment-intent) endpoint with the currency, blockchain, payment intent type, and (for transient only) amount. Your server receives the deposit address by using [webhooks](/circle-mint/circle-apis-notifications-quickstart) or by polling the [Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent) endpoint until `paymentMethods` includes an `address`. Your customer-facing UI shows the deposit address (and for transient only, the amount). The customer sends USDC or EURC from their wallet to that address on the correct blockchain. Circle detects the transfer. It creates or updates payment data and sends notifications. Your server treats the payin as complete when payment and payment intent state match your policies. The following diagram illustrates the payin flow at checkout: ```mermaid theme={null} sequenceDiagram participant C as Customer participant W as Customer-facing UI participant S as Your server participant M as Circle Mint APIs Note over C,M: Create payment intent C->>W: Chooses to pay with stablecoins on a blockchain W->>S: Requests onchain deposit S->>M: Sends payment intent creation request M-->>S: Sends payment intent Note over C,M: Send and display deposit address M-->>S: Sends notification containing:
payment intent status (pending),
deposit address S->>W: Sends deposit address W->>C: Displays deposit address Note over C,M: Customer pays onchain and Circle notifies your server C->>M: Transfers funds to deposit address M-->>S: Sends notification containing:
payment status (paid) M-->>S: Sends notification containing:
payment intent status (complete) Note over C,M: Confirm payment to customer S->>W: Sends payment confirmation W->>C: Displays payment confirmation ``` ## Payout flow To [send a stablecoin payout](/circle-mint/send-stablecoin-payout), your server calls the Crypto Payouts API. The recipient receives USDC or EURC at the wallet address you register. Your server creates a recipient by calling the [Create address book recipient](/api-reference/circle-mint/payouts/create-address-book-recipient) endpoint with the destination blockchain and address. The recipient status must be `active` before continuing. Your server calls the [Create payout](/api-reference/circle-mint/payouts/create-payout) endpoint with the address book recipient ID, `sourceWalletId`, and amounts. The payout starts in `pending`. Circle debits your Mint balance and sends funds to the registered address on the selected blockchain. Treat the payout as finalized when the `status` is `complete`. Use [webhooks](/circle-mint/circle-apis-notifications-quickstart) for `payout` events or poll the [Get payout](/api-reference/circle-mint/payouts/get-payout) endpoint. The following diagram illustrates the payout flow: ```mermaid theme={null} sequenceDiagram participant S as Your server participant M as Circle Mint APIs participant R as Recipient wallet Note over S,M: Create address book recipient S->>M: Sends create address book recipient request M-->>S: Sends address book recipient Note over S,M: Create payout S->>M: Sends create payout request M-->>S: Sends payout (pending) Note over M,R: Onchain delivery M->>R: Transfers USDC or EURC to registered address Note over S,M: Payout complete M-->>S: Sends notification containing:
payout status (complete) ``` ## Payment intent expiration A payment intent can expire. Expiration does not automatically return funds. The intent mainly tells the customer which deposit address to use. If funds still arrive at that address, Circle processes it as a payin on your Mint account. ## Refunds [Issue a full or partial refund](/circle-mint/refund-stablecoin-payin) through the refund APIs or by signing in to your [Circle Mint](https://app.circle.com/signin) account. What you should know about refunds: * Start a refund after at least one payment on the intent has completed and settled. You cannot start a refund while a payment is still pending. * Submit refunds within 30 days of creating the payment intent. During that period you can send more than one partial refund, up to the limits of your Mint account. * After you start a refund, the payment intent status moves to `refunded`. The intent stops accepting new payins. Don't reuse a refunded intent for new checkouts. Funds sent to that address might not match the intent and can require support. * If you use your Circle Mint account to refund, the refund cannot be canceled once submitted. # How-to: Deposit Fiat Source: https://developers.circle.com/circle-mint/howtos/deposit-fiat Link a bank account and deposit fiat to mint USDC or EURC in your Circle Mint account. Deposit fiat (onramp) from an external bank account to mint USDC or EURC in your Circle Mint balance. The `/wires` endpoint supports multiple payment rails, including standard wires (FedWire and SWIFT), real-time interbank rails (RTP, SPEI, SEPA, and CHATS) where available, and book transfers when you bank with one of Circle's settlement partners. This guide focuses on standard wire deposits. The same endpoint handles other rails -- Circle routes the deposit based on your linked bank and region. Rail-specific parameters and additional endpoints (such as CUBIX and PIX for local currencies) are out of scope here. ## Prerequisites Before you begin: * Complete the [account and API key setup](/circle-mint/quickstarts/getting-started). * Have access to a bank account that can send wire transfers. ## Step 1. Link a bank account Use the [create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account) endpoint to register your external bank account with Circle Mint. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/banks/wires \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey":"ba943ff1-ca16-49b2-ba55-1057e70ca5c7","accountNumber":"12340010","routingNumber":"121000248","billingDetails":{"name":"Satoshi Nakamoto","city":"Boston","country":"US","line1":"100 Money Street","district":"MA","postalCode":"01234"},"bankAddress":{"bankName":"SAN FRANCISCO","city":"SAN FRANCISCO","country":"US","line1":"100 Money Street","district":"CA"}}' ``` Expected response: ```json theme={null} { "data": { "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "status": "pending", "description": "WELLS FARGO BANK, NA ****0010", "trackingRef": "CIR2GKYL4B", "virtualAccountEnabled": true, "billingDetails": { "name": "Satoshi Nakamoto", "line1": "100 Money Street", "city": "Boston", "postalCode": "01234", "district": "MA", "country": "US" }, "bankAddress": { "bankName": "WELLS FARGO BANK, NA", "line1": "100 Money Street", "city": "SAN FRANCISCO", "district": "CA", "country": "US" }, "createDate": "2023-11-04T20:02:21.062Z", "updateDate": "2023-11-04T20:02:21.062Z" } } ``` Record the `id` and `trackingRef` values from the response. You use both in the following steps. ## Step 2. Retrieve wire instructions Use the [get wire instructions](/api-reference/circle-mint/account/get-business-wire-account-instructions) endpoint to fetch the beneficiary details your bank needs to send the wire. ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/banks/wires/${BANK_ACCOUNT_ID}/instructions \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": { "trackingRef": "CIR22FEP33", "beneficiary": { "name": "CIRCLE INTERNET FINANCIAL INC", "address1": "1 MAIN STREET", "address2": "SUITE 1" }, "virtualAccountEnabled": true, "beneficiaryBank": { "name": "CRYPTO BANK", "address": "1 MONEY STREET", "city": "NEW YORK", "postalCode": "1001", "country": "US", "swiftCode": "CRYPTO99", "routingNumber": "999999999", "accountNumber": "123815146304", "currency": "USD" } } } ``` The response includes: * **Beneficiary details** -- the name and address of the recipient (Circle). * **Routing information** -- the bank name, SWIFT code, and routing number for the beneficiary bank. * **Virtual Account Number** -- the `beneficiaryBank.accountNumber` field, unique to your linked bank account. * **Tracking reference** -- the `trackingRef` value to include in the wire memo. ### Virtual account numbers Not all Circle settlement banks support Virtual Account Numbers. When `virtualAccountEnabled` is `true` in the wire instructions response, the linked bank account has a unique VAN in the `beneficiaryBank.accountNumber` field. When the sender includes the VAN as the account number on their wire, the tracking reference in the wire memo becomes optional. When `virtualAccountEnabled` is `false`, the sender must include the `trackingRef` in the wire memo so Circle can match the deposit to your account. Benefits of using a VAN (where supported): * Eliminates the need for senders to include tracking references in payment instructions. * Reduces wire returns caused by missing or incorrect tracking references. * Supports all wire types: domestic, international, and SWIFT. ## Step 3. Send the wire deposit In production, initiate the wire transfer from your bank using the instructions returned in Step 2. Confirm that the wire details -- beneficiary name, account number, and routing number -- match exactly. Mismatched details can result in wire returns or processing delays of several business days. Domestic wire deposits received before the daily cutoff typically settle on the same business day. International wires may take longer depending on intermediary banks. ### Simulate a deposit in sandbox In the sandbox environment, use the [mock wire payment](/api-reference/circle-mint/account/create-mock-wire-payment) endpoint to simulate a wire deposit without sending real funds. Provide the `trackingRef` from Step 2 and the `beneficiaryBank.accountNumber` (the VAN) from the wire instructions response. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/mocks/payments/wire \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"amount":{"amount":"50.00","currency":"USD"},"trackingRef":"CIR22FEP33","beneficiaryBank":{"accountNumber":"123815146304"}}' ``` Expected response: ```json theme={null} { "data": { "trackingRef": "CIR22FEP33", "amount": { "amount": "50.00", "currency": "USD" }, "beneficiaryBank": { "accountNumber": "123815146304" }, "status": "pending" } } ``` The mock wire endpoint is available in sandbox only. Mock wire deposits process in batches and may take up to 15 minutes to complete. ## Step 4. Verify the deposit Use the [list deposits](/api-reference/circle-mint/account/list-business-deposits) endpoint to confirm the incoming fiat deposit has settled. ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/deposits \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": [ { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "sourceWalletId": "1000066041", "destination": { "type": "wallet", "id": "1000066041" }, "amount": { "amount": "50.00", "currency": "USD" }, "status": "complete", "createDate": "2024-01-01T12:00:00.000Z" } ] } ``` ## See also * [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) -- understand the minting process * [Sandbox to Production](/circle-mint/references/sandbox-and-testing) -- transition to production * [Create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account) \-- API reference # How-to: Transfer USDC Onchain Source: https://developers.circle.com/circle-mint/howtos/transfer-on-chain Receive USDC and EURC via deposit addresses and send them to external blockchain wallets using the Circle Mint API. Receive USDC or EURC by generating deposit addresses for external wallets to send to, or send USDC or EURC to allowlisted recipient addresses on supported blockchains. Transfers of \$3,000 or more require Travel Rule compliance data for third-party payouts. ## Prerequisites Before you begin: * Complete the [account and API key setup](/circle-mint/quickstarts/getting-started). * Review [supported chains and currencies](/circle-mint/references/supported-chains-and-currencies) for available blockchains. * (For sending) Have a funded Circle Mint account. * (For sending) All recipient addresses must be approved by an account administrator through the [Mint Console](https://app.circle.com/signin) before you can create transfers. If your account is domiciled in France or Singapore, addresses require additional verification through the Mint Console. ## Step 1. Receive USDC via deposit address ### Step 1.1. Create a deposit address Use the create deposit address endpoint to generate an address for receiving USDC or EURC on a specific blockchain. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/deposit \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "currency": "USD", "chain": "ARC"}' ``` Expected response: ```json theme={null} { "data": { "id": "d51d72d2-9955-4340-b3fd-2f07a82a1e6c", "address": "0xbd01242af414961c25aa72dcae06646fc52e9b92", "currency": "USD", "chain": "ARC" } } ``` You can create one deposit address per blockchain. Use the same address for all deposits on that blockchain. ### Step 1.2. Send funds to your deposit address This step happens outside the Circle Mint API. The sender transfers USDC or EURC from their external wallet to your deposit address on the matching blockchain. Sending funds on the wrong blockchain results in permanent loss. Always confirm the blockchain network matches between the sender's wallet and your deposit address. ### Step 1.3. Verify the deposit Use the [list transfers](/api-reference/circle-mint/account/list-business-transfers) endpoint to confirm the incoming transfer has settled. ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/transfers \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": [ { "id": "a6a1b575-13d5-4e73-9da7-73e2a3e4418a", "source": { "type": "blockchain", "chain": "ARC" }, "destination": { "type": "wallet", "id": "1000066041" }, "amount": { "amount": "100.00", "currency": "USD" }, "transactionHash": "0x4cfd25b5ab46e9fe25e845e7a7e0ea2f1f7e4bba3c6e0f1db0b846e4a1bc5fd2", "status": "complete", "createDate": "2024-01-01T12:00:00.000Z" } ] } ``` The transfer reaches `complete` status after the required number of [blockchain confirmations](/circle-mint/references/blockchain-confirmations) for the deposit's blockchain. ## Step 2. Send USDC to an external address ### Step 2.1. Add a recipient address Use the create recipient address endpoint to allowlist an external blockchain address for outbound transfers. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/recipient \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey": "2a308497-e66e-4c42-ac1e-7bedab86d958", "address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1", "chain": "ARC", "currency": "USD", "description": "Treasury wallet"}' ``` Expected response: ```json theme={null} { "data": { "id": "cfa01bb0-d166-5506-a48a-56f2beab559f", "address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1", "chain": "ARC", "currency": "USD", "description": "Treasury wallet" } } ``` Adding a recipient address through the API creates a pending request. An account administrator must approve the address through the [Mint Console](https://app.circle.com/signin) before you can send transfers to it. A confirmation notification is sent to all administrators. ### Step 2.2. Create a transfer Use the create transfer endpoint to send funds to the approved recipient address. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/transfers \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey": "6ec3827d-15bb-442e-9d4c-32e73e61cbf4", "destination": {"type": "verified_blockchain", "addressId": "cfa01bb0-d166-5506-a48a-56f2beab559f"}, "amount": {"currency": "USD", "amount": "25.00"}}' ``` Expected response: ```json theme={null} { "data": { "id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea", "source": { "type": "wallet", "id": "1016875042" }, "destination": { "type": "blockchain", "address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1", "chain": "ARC" }, "amount": { "amount": "25.00", "currency": "USD" }, "status": "pending", "createDate": "2024-07-15T16:41:12.395Z" } } ``` ### Step 2.3. Check the transfer status Use the get transfer endpoint to monitor the status of your transfer. ```bash theme={null} curl -X GET https://api-sandbox.circle.com/v1/businessAccount/transfers/21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` The transfer progresses through these statuses: * **`pending`**: Circle received the request. * **`running`**: The transaction is broadcast onchain. The response includes a `transactionHash`. * **`complete`**: Blockchain finality reached -- the required number of confirmations passed. Expected response for a running transfer: ```json theme={null} { "data": { "id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea", "source": { "type": "wallet", "id": "1016875042" }, "destination": { "type": "blockchain", "address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1", "chain": "ARC" }, "amount": { "amount": "25.00", "currency": "USD" }, "transactionHash": "0x0654eee4f609f9c35e376cef9455dd9fc1546c482c5c32c8f8d434ead14fcf97", "status": "running", "createDate": "2024-07-15T16:41:12.395Z" } } ``` ## Travel Rule compliance The Financial Crimes Enforcement Network (FinCEN) Travel Rule requires financial institutions to share originator and beneficiary information for transfers of \$3,000 or more. ### When it applies Travel Rule applies to onchain transfers of \$3,000 or more on these blockchains: * Algorand (ALGO) * Aptos (APTOS) * Arbitrum (ARB) * Avalanche (AVAX) * Base (BASE) * Celo (CELO) * Ethereum (ETH) * NEAR (NEAR) * Optimism (OP) * Polygon PoS (POLY) * Ripple (XRPL) * Solana (SOL) * Stellar (XLM) ### Business account transfers For transfers from your Circle Mint account using `POST /v1/businessAccount/transfers`, Circle uses your company identity on file. No additional data is required in the API request. ### Third-party payouts For payouts to third parties using the [Crypto Payouts API](/circle-mint/send-stablecoin-payout) (`POST /v1/payouts`), you must include the originator's identity in the `source.identities` array. The following table describes the identities schema: | Field | Type | Required | Description | | ------------------------ | ------ | --------------------------- | --------------------------------------- | | `type` | string | Yes | `individual` or `business` | | `name` | string | Yes | Full legal name | | `addresses` | array | No | Array of address objects | | `addresses[].line1` | string | Yes (if addresses provided) | Street address | | `addresses[].city` | string | Yes (if addresses provided) | City | | `addresses[].district` | string | No | State/province (2-letter code for `US`) | | `addresses[].country` | string | Yes (if addresses provided) | ISO 3166-1 alpha-2 country code | | `addresses[].postalCode` | string | No | Postal code | The following example shows a payout request with originator identity data: ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/payouts \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "source": {"type": "wallet", "id": "12345", "identities": [{"type": "individual", "name": "Satoshi Nakamoto", "addresses": [{"line1": "100 Money Street", "city": "Boston", "district": "MA", "country": "US", "postalCode": "01234"}]}]}, "destination": {"type": "blockchain", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "chain": "ETH"}, "amount": {"amount": "3000.00", "currency": "USD"}}' ``` Expected response: ```json theme={null} { "data": { "id": "b36cbf12-6ed1-47ed-9eb9-5874f8991ca8", "source": { "type": "wallet", "id": "12345", "identities": [ { "type": "individual", "name": "Satoshi Nakamoto", "addresses": [ { "line1": "100 Money Street", "city": "Boston", "district": "MA", "country": "US", "postalCode": "01234" } ] } ] }, "destination": { "type": "blockchain", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "chain": "ETH" }, "amount": { "amount": "3000.00", "currency": "USD" }, "status": "pending", "createDate": "2024-07-10T02:13:30.000Z" } } ``` ### Receiving transfers If you are a regulated financial institution, Circle provides originator identity data on inbound transfers of \$3,000 or more. Use `returnIdentities=true` as a query parameter on GET endpoints: ```bash theme={null} curl -X GET "https://api-sandbox.circle.com/v1/payouts/{id}?returnIdentities=true" \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` ### Failure behavior If a transfer requires identity data and it is omitted, the transfer fails with: * `riskEvaluation.decision`: `denied` * `riskEvaluation.reason`: `3220` ## See also * [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) -- understand the transfer lifecycle * [Blockchain Confirmations](/circle-mint/references/blockchain-confirmations) \-- confirmation counts by blockchain * [Supported Chains and Currencies](/circle-mint/references/supported-chains-and-currencies) \-- available blockchains # How-to: Withdraw Fiat Source: https://developers.circle.com/circle-mint/howtos/withdraw-fiat Redeem USDC or EURC to fiat and withdraw funds to your bank account using the Circle Mint API. Redeem (offramp) USDC or EURC in your Circle Mint balance to fiat and send funds to a linked bank account. You can track payout status from `pending` through `complete` or `failed`, and handle returned withdrawals caused by bank-side rejections. Payouts route through the `/wires` endpoint and support standard wires, real-time interbank rails (RTP, SPEI, SEPA, CHATS) where available, and book transfers when applicable -- Circle selects the rail based on your destination bank and region. ## Prerequisites Before you begin: * Complete the [account and API key setup](/circle-mint/quickstarts/getting-started). * Have a funded Circle Mint account with available USDC or EURC balance. * Have a linked bank account. If you have not linked one, see [Deposit Fiat](/circle-mint/howtos/deposit-fiat) Step 1. * If your Circle Mint account is domiciled in Singapore or France, verify your payout recipients through the [Mint Console](https://app.circle.com/signin) before proceeding. Unverified recipients cause payouts to remain in `pending` status. ## Step 1. Verify your balance Before you initiate a withdrawal, confirm that your available balance covers the amount you plan to send. ```bash theme={null} curl -X GET https://api-sandbox.circle.com/v1/businessAccount/balances \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" ``` Expected response: ```json theme={null} { "data": { "available": [ { "amount": "150.00", "currency": "USD" } ], "unsettled": [ { "amount": "25.00", "currency": "USD" } ] } } ``` The `available` array shows funds you can withdraw immediately. The `unsettled` array shows funds that are still being processed and are not yet available. ## Step 2. Create a payout Use the [create a payout](/api-reference/circle-mint/account/create-business-payout) endpoint to send funds from your Circle Mint account to your linked bank account. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/payouts \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey": "'$(uuidgen)'", "destination": {"type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636"}, "amount": {"currency": "USD", "amount": "75.00"}}' ``` Replace the `destination.id` value with the bank account ID returned when you linked your bank account. Expected response: ```json theme={null} { "data": { "id": "9cf38c76-cac4-40d8-a516-f46e9a610a85", "amount": { "amount": "75.00", "currency": "USD" }, "status": "pending", "sourceWalletId": "1016875042", "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "name": "WELLS FARGO BANK, NA ****0010" }, "createDate": "2024-01-15T14:22:31.062Z", "updateDate": "2024-01-15T14:22:31.062Z" } } ``` Record the `id` from the response to check the payout status in the next step. ## Step 3. Check the payout status Use the [get a payout](/api-reference/circle-mint/account/get-business-payout) endpoint to check the current status of your withdrawal. ```bash theme={null} curl -X GET https://api-sandbox.circle.com/v1/businessAccount/payouts/9cf38c76-cac4-40d8-a516-f46e9a610a85 \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" ``` A payout moves through the following statuses: * **`pending`**: Circle has received the payout request and is processing it. * **`complete`**: Funds have been sent to the receiving bank. * **`failed`**: The payout could not be processed. Check the `errorCode` field for details. Expected response for a completed payout: ```json theme={null} { "data": { "id": "9cf38c76-cac4-40d8-a516-f46e9a610a85", "amount": { "amount": "75.00", "currency": "USD" }, "status": "complete", "sourceWalletId": "1016875042", "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "name": "WELLS FARGO BANK, NA ****0010" }, "createDate": "2024-01-15T14:22:31.062Z", "updateDate": "2024-01-16T09:15:42.778Z" } } ``` Payouts are asynchronous. To track status changes without polling, subscribe to `payouts` webhook notifications. Alternatively, poll the get a payout endpoint at a reasonable interval until the status reaches `complete` or `failed`. ### Returned withdrawals Even after a payout reaches `complete` status, bank-side issues can cause the wire to be returned. Common reasons include: * Incorrect account details, such as a wrong routing number or a closed account. * Compliance holds at the receiving bank. * Beneficiary name mismatch between the payout and the bank account on file. When a wire is returned, the funds are re-credited to your Circle Mint balance. Monitor webhook notifications for `payouts` events to detect returns. If a payout fails or is returned, verify the bank account details and retry with a new idempotency key. ## See also * [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) -- understand the redemption process * [Sandbox to Production](/circle-mint/references/sandbox-and-testing) -- production settlement timing differences * [Create a payout](/api-reference/circle-mint/account/create-business-payout) \-- API reference # Notifications Data Models Source: https://developers.circle.com/circle-mint/notifications-data-models Learn about the events and data models used in Circle Notifications. Circle API notifications are subscriber endpoints that enable you to receive notifications every time the status of a resource changes. ## Common attributes All notification messages have the following attributes: | Name | Type | Description | Sample | | :----------------- | :---------------- | :---------------------------- | :------------------------------------- | | `clientId` | `string` (UUIDv4) | Client identifier | `c60d2d5b-203c-45bb-9f6e-93641d40a599` | | `notificationType` | `string` | The type of notification | `payouts` | | `version` | `int` | The version of the data model | `1` | ## Implementations This section lists all notification models. ### Payout #### Completed *Completed* payouts are settled payouts. Therefore, the funds should be available in the destination wallet. The following structure represents the notifications for completed payouts. ```json Completed Payout Notification Payload theme={null} { "clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599", "notificationType": "payouts", "payout": {...} } ``` The `payout` payload is a [payout object](/circle-mint/circle-api-resources#payout-object). #### Failed Failed payouts notifications are structured as follows: ```json Failed Payout Notification Payload theme={null} { "clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599", "notificationType": "payouts", "payout": {...} } ``` The `payout` payload is a [payout object](/circle-mint/circle-api-resources#payout-object). ### Transfer #### Created A notification with the structure below is sent on transfer creation. ```json Created Transfer Notification Payload theme={null} { "clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599", "notificationType": "transfers", "transfer": {...} } ``` The `transfer` payload is a [transfer object](/circle-mint/circle-api-resources#transfer-object). #### Failed Failed transfers notifications are structured as follows: ```json Failed Transfer Notification Payload theme={null} { "clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599", "notificationType": "transfers", "transfer": {...} } ``` The `transfer` payload is a [transfer object](/circle-mint/circle-api-resources#transfer-object). #### Completed Completed transfers notifications are structured as follows: ```json Completed Transfer Notification Payload theme={null} { "clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599", "notificationType": "transfers", "transfer": {...} } ``` The `transfer` payload is a [transfer object](/circle-mint/circle-api-resources#transfer-object). # Postman Suite Source: https://developers.circle.com/circle-mint/postman Use Circle's Postman collections to easily send API requests and try out our APIs. *Circle's Postman collection holds sample requests for Circle APIs. Run them in Postman, an API client. The Circle Mint workspace has one collection per area: API Overview, Core Functionality, Stablecoin Payins, and Stablecoin Payouts. Folders follow the same layout as the [API References](/api-reference/circle-mint/general/ping).* ## How To Use It ### Run in Postman Select one of the six **Run in Postman** links below. You'll be asked whether you want to fork the collection to your workspace, view the collection in the public workspace, or import the collection into Postman. Fork: Creates a copy of the collection while maintaining a link to the parent. View: Allows you to quickly try out the API without having to import anything into your Postman suite. Import: Creates a copy of the collection but does not maintain a link to Circle's copy. | Collection | Run it! | | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | API Overview | [![Run in Postman](https://run.pstmn.io/button.svg)](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799579019\&usg=AOvVaw1J5pjlBnXW56T9Jg1Jem4R) | | Core Functionality | [![Run in Postman](https://run.pstmn.io/button.svg)](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799581882\&usg=AOvVaw2MrbS0b_WrIS2xff7v4UMs) | | Crypto Deposits API | [![Run in Postman](https://run.pstmn.io/button.svg)](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799584756\&usg=AOvVaw1lqKomXCegg8MBf7mppUOm) | | Crypto Payouts API | [![Run in Postman](https://run.pstmn.io/button.svg)](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-53206224-cf33-4bbd-8e8a-09d5a780795a?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-53206224-cf33-4bbd-8e8a-09d5a780795a%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799587291\&usg=AOvVaw3tZLn5_kLwqQDkwELUpt3M) | **Authorization** To authorize your session authorization, use Circle's Postman variable `apiKey` and add your API key to the `environment` or `collection` variables. See Postman's [using variables](https://learning.postman.com/docs/sending-requests/variables/) for details. Need a Circle sandbox API Key? Sign up for a [Circle account](https://app-sandbox.circle.com/signup)—it only takes a minute or two. # Set Up Your Account and API Key Source: https://developers.circle.com/circle-mint/quickstarts/getting-started Create a sandbox account, generate an API key, and make your first Circle Mint API request. This guide walks you through creating a sandbox account, generating an API key, and verifying that you can connect to the Circle Mint API. ## Prerequisites Before you begin, ensure you have: * A valid email address to register for a Circle Mint sandbox account * [curl](https://curl.se/) or another tool for making HTTP requests ## Step 1: Create a sandbox account The sandbox environment lets you test API integrations without processing real transactions. For more details on sandbox versus production environments, see [Sandbox to Production](/circle-mint/references/sandbox-and-testing). Go to [app-sandbox.circle.com/signup](https://app-sandbox.circle.com/signup) and complete the registration form. Check your inbox and confirm your email to activate your account. After activation, log in at `https://app-sandbox.circle.com`. ## Step 2: Generate an API key Circle Mint uses API keys to authenticate all requests. Create and manage keys in the [Mint Console](https://app-sandbox.circle.com/developer). Go to [app-sandbox.circle.com/developer](https://app-sandbox.circle.com/developer). Restrict where the key can be used from. API keys grant access to privileged operations on Circle APIs. Store your API key securely and never expose it in client-side code, public repositories, or other publicly accessible locations. All API requests must be made over HTTPS. You can create a maximum of 10 API keys per environment. ## Step 3: Test connectivity Test raw connectivity by calling the `/ping` endpoint. This endpoint does not require authentication, so it confirms that your application can reach the API. ```bash theme={null} curl -s https://api-sandbox.circle.com/ping ``` If your application reached the API, you see the following response: ```json theme={null} { "message": "pong" } ``` ## Step 4: Verify your API key Circle Mint uses Bearer token authentication. Include your API key in the `Authorization` header of every request using the format `Bearer YOUR_API_KEY`. Call the `/v1/configuration` endpoint to confirm that your API key is valid and properly configured: ```bash theme={null} curl -s https://api-sandbox.circle.com/v1/configuration \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` A successful response returns your account configuration, including your `masterWalletId`. The `masterWalletId` is the identifier for the primary wallet associated with your Circle Mint account, used as the source or destination in transfer and payout operations. ```json theme={null} { "data": { "payments": { "masterWalletId": "1234567890" } } } ``` If the API key is missing or malformed, you receive a `401 Unauthorized` error: ```json theme={null} { "code": 401, "message": "Malformed authorization. Are the credentials properly encoded?" } ``` If you see this error, verify that your `Authorization` header uses the `Bearer` prefix and that you copied the full API key from the Mint Console. # Quickstart: Mint and Redeem USDC Source: https://developers.circle.com/circle-mint/quickstarts/mint-and-redeem Deposit fiat to mint USDC, transfer it onchain, and redeem USDC back to fiat using the Circle Mint API. This guide walks you through a complete mint-and-redeem cycle in the Circle Mint sandbox: link a bank account, deposit fiat to mint USDC, transfer USDC onchain, and redeem USDC back to fiat. ## Prerequisites Before you begin, complete the [account and API key setup](/circle-mint/quickstarts/getting-started). Replace `${YOUR_API_KEY}` in the examples below with your sandbox API key. ## Step 1: Create a bank account Register a mock bank account using the [create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account) endpoint. This bank account serves as the source for depositing fiat and the destination for redeeming USDC. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/banks/wires \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "unique-id-1", "accountNumber": "12340010", "routingNumber": "121000248", "billingDetails": { "name": "Satoshi Nakamoto", "city": "Boston", "country": "US", "line1": "100 Money Street", "district": "MA", "postalCode": "01234" }, "bankAddress": { "bankName": "WELLS FARGO BANK, NA", "city": "San Francisco", "country": "US", "line1": "420 Montgomery Street", "district": "CA" } }' ``` Expected response: ```json theme={null} { "data": { "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "status": "pending", "description": "WELLS FARGO BANK, NA ****0010", "trackingRef": "CIR2GKYL4B", "fingerprint": "a9a71b77-d83d-4fbc-997f-41a33550c594", "virtualAccountEnabled": true, "billingDetails": { "name": "Satoshi Nakamoto", "line1": "100 Money Street", "city": "Boston", "postalCode": "01234", "district": "MA", "country": "US" }, "bankAddress": { "bankName": "WELLS FARGO BANK, NA", "city": "SAN FRANCISCO", "district": "CA", "country": "US" }, "createDate": "2026-01-15T12:00:00.000Z", "updateDate": "2026-01-15T12:00:00.000Z" } } ``` Save the bank account `id` from the response. You need it in the following steps. ## Step 2: Get wire instructions Retrieve the wire instructions for your bank account using the [get wire instructions](/api-reference/circle-mint/account/get-business-wire-account-instructions) endpoint. The response includes the `trackingRef` and beneficiary `accountNumber` you need to simulate a wire deposit. ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/banks/wires/9d1fa351-b24d-442a-8aa5-e717db1ed636/instructions \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": { "trackingRef": "CIR2GKYL4B", "beneficiary": { "name": "CIRCLE INTERNET FINANCIAL INC", "address1": "99 High Street", "address2": "Boston, MA 02110" }, "beneficiaryBank": { "name": "CUSTOMERS BANK", "routingNumber": "031101279", "accountNumber": "123815146304", "city": "Phoenixville", "postalCode": "19460", "country": "US" } } } ``` Save the `trackingRef` and the `beneficiaryBank.accountNumber` values for the next step. ## Step 3: Deposit fiat to mint USDC Simulate a wire deposit using the [create a mock wire payment](/api-reference/circle-mint/account/create-mock-wire-payment) endpoint. In the sandbox, this mints USDC into your Circle Mint account without moving real funds. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/mocks/payments/wire \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "amount": { "amount": "100.00", "currency": "USD" }, "trackingRef": "CIR2GKYL4B", "beneficiaryBank": { "accountNumber": "123815146304" } }' ``` Expected response: ```json theme={null} { "data": { "trackingRef": "CIR2GKYL4B", "amount": { "amount": "100.00", "currency": "USD" }, "status": "pending" } } ``` Sandbox mock wire deposits process in batches and may take up to 15 minutes to complete. Wait for the deposit to settle before continuing. After the deposit settles, verify your balance using the [list all balances](/api-reference/circle-mint/account/list-business-balances) endpoint: ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/balances \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": { "available": [{ "amount": "100.00", "currency": "USD" }], "unsettled": [] } } ``` The `available` balance confirms that your fiat deposit minted USDC successfully. ## Step 4: Transfer USDC onchain Send USDC from your Circle Mint account to an external blockchain address. This step requires two API calls: create a recipient address, then create a transfer. ### 4.1. Create a recipient address Register a destination address using the [create a recipient address](/api-reference/circle-mint/account/create-business-recipient-address) endpoint. This example uses the Ethereum blockchain. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/recipient \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "unique-id-2", "address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1", "chain": "ETH", "currency": "USD", "description": "External Ethereum wallet" }' ``` Expected response: ```json theme={null} { "data": { "id": "cfa01bb0-d166-5506-a48a-56f2beab559f", "address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1", "chain": "ETH", "currency": "USD", "description": "External Ethereum wallet" } } ``` ### 4.2. Create a transfer Send USDC to the recipient address using the [create a transfer](/api-reference/circle-mint/account/create-business-transfer) endpoint: ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/transfers \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "unique-id-3", "destination": { "type": "verified_blockchain", "addressId": "cfa01bb0-d166-5506-a48a-56f2beab559f" }, "amount": { "currency": "USD", "amount": "25.00" } }' ``` Expected response: ```json theme={null} { "data": { "id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea", "source": { "type": "wallet", "id": "1016875042" }, "destination": { "type": "blockchain", "address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1", "chain": "ETH" }, "amount": { "amount": "25.00", "currency": "USD" }, "status": "pending" } } ``` The transfer starts in `pending` status, moves to `running` once broadcast onchain, and reaches `complete` after enough [blockchain confirmations](/circle-mint/references/blockchain-confirmations). ## Step 5: Redeem USDC to fiat Convert your remaining USDC balance back to fiat by creating a payout to the bank account you registered in Step 1. Use the [create a payout](/api-reference/circle-mint/account/create-business-payout) endpoint: ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/payouts \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "unique-id-4", "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636" }, "amount": { "currency": "USD", "amount": "75.00" } }' ``` Expected response: ```json theme={null} { "data": { "id": "9cf38c76-cac4-40d8-a516-f46e9a610a85", "amount": { "amount": "75.00", "currency": "USD" }, "status": "pending", "sourceWalletId": "1016875042", "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "name": "WELLS FARGO BANK, NA ****0010" } } } ``` ## Step 6: Verify the round trip Check your final balance to confirm both the transfer and payout processed: ```bash theme={null} curl https://api-sandbox.circle.com/v1/businessAccount/balances \ -H "Authorization: Bearer ${YOUR_API_KEY}" ``` Expected response: ```json theme={null} { "data": { "available": [{ "amount": "0.00", "currency": "USD" }], "unsettled": [] } } ``` You completed the full Circle Mint cycle: 1. Deposited \$100 USD via wire to mint 100 USDC. 2. Transferred 25 USDC onchain to an Ethereum address. 3. Redeemed 75 USDC back to fiat via wire payout. Your available balance returns to zero, confirming every dollar is accounted for. The mock wire endpoint used in Step 3 is only available in the sandbox. In production, initiate wire transfers from your own banking interface using the wire instructions from Step 2. All other API calls in this guide work the same in production. See [Sandbox to Production](/circle-mint/references/sandbox-and-testing) for details on transitioning. # Quickstart: Receive a Stablecoin Payin Source: https://developers.circle.com/circle-mint/receive-stablecoin-payin Create a payment intent, obtain a deposit address, and confirm onchain USDC or EURC payins using the Crypto Deposits API. Use the Crypto Deposits API to accept onchain USDC or EURC [stablecoin payins](/circle-mint/how-stablecoin-payins-and-payouts-work) into Circle Mint from your customers' wallets. You create the payment intent, share the deposit address, have the customer send funds, then confirm the transfer using the API or webhooks. The examples in this quickstart use Ethereum, but you can use any [supported blockchain](/circle-mint/references/supported-chains-and-currencies). ## Prerequisites Before you begin this tutorial, ensure you've: * Obtained a [Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with access to the Crypto Deposits API (stablecoin payins). * Obtained a `merchantWalletId` for the wallet that receives settled funds. * Installed cURL for API calls. * (Optional) Configured [webhook notifications](/circle-mint/circle-apis-notifications-quickstart). ## Step 1: Create a payment intent Call [Create a payment intent](/api-reference/circle-mint/payments/create-payment-intent) when the customer chooses to pay with USDC or EURC onchain. Choose continuous or transient mode. **Expected result:** a response with a payment intent `id` and `timeline` including `created`. Continuous payment intents are the default type. You do not include an `amount` at creation. Example request: ```bash cURL theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd", "currency": "USD", "settlementCurrency": "USD", "merchantWalletId": "${MERCHANT_WALLET_ID}", "paymentMethods": [ { "type": "blockchain", "chain": "ETH" } ], "type": "continuous" }' ``` Example response: ```json JSON theme={null} { "data": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "0.00", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH" } ], "timeline": [ { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-21T20:13:35.578678Z" } } ``` Transient intents include a specific `amount` and are intended for a single checkout payment. You must set the `type` to `transient`. Example request: ```bash cURL theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd", "type": "transient", "amount": { "amount": "1.00", "currency": "USD" }, "settlementCurrency": "USD", "merchantWalletId": "${MERCHANT_WALLET_ID}", "paymentMethods": [ { "type": "blockchain", "chain": "ETH" } ] }' ``` Example response: ```json JSON theme={null} { "data": { "id": "6e4d4047-db14-4c09-b238-1215aee50d03", "amount": { "amount": "1.00", "currency": "USD" }, "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ETH" } ], "paymentIds": [], "timeline": [ { "status": "created", "time": "2022-07-21T20:13:35.579331Z" } ], "createDate": "2022-07-21T20:13:35.578678Z", "updateDate": "2022-07-21T20:19:24.859052Z" } } ``` ## Step 2: Obtain the deposit address Circle does not return the deposit address in the create response. Use webhooks or poll [Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent) until `paymentMethods.address` is set. **Expected result:** you have the onchain deposit address to show the customer. After you [subscribe to notifications](/circle-mint/circle-apis-notifications-quickstart) for `paymentIntents`, you receive updates when the payment intent changes. When `paymentMethods.address` is set, the payload includes an updated `timeline` of the historical payment intent statuses and timestamps. **Expected result:** the notification includes `paymentIntent.paymentMethods` with `address` and `timeline` with `pending` after the address is assigned. Example `paymentIntents` notification after the deposit address is assigned: ```json Continuous theme={null} { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "notificationType": "paymentIntents", "version": 1, "customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" }, "paymentIntent": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" } ], "paymentIds": [], "refundIds": [], "timeline": [ { "status": "pending", "time": "2023-01-21T20:13:38.188286Z" }, { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-21T20:13:38.186831Z" } } ``` ```json Transient theme={null} { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "notificationType": "paymentIntents", "version": 1, "customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" }, "paymentIntent": { "id": "6e4d4047-db14-4c09-b238-1215aee50d03", "amount": { "amount": "1.00", "currency": "USD" }, "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" } ], "paymentIds": [], "refundIds": [], "timeline": [ { "status": "pending", "time": "2022-07-21T20:13:38.188286Z" }, { "status": "created", "time": "2022-07-21T20:13:35.579331Z" } ], "createDate": "2022-07-21T20:13:35.578678Z", "updateDate": "2022-07-21T20:13:38.186831Z", "expiresOn": "2022-07-21T21:13:38.087275Z" } } ``` Call the [Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent) endpoint until `paymentMethods.address` is present in the response. **Expected result:** the response `data` includes `paymentMethods` with `address` for the deposit. Example request: ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/{id}' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response after the address is available: ```json Continuous theme={null} { "data": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" } ], "paymentIds": [], "refundIds": [], "timeline": [ { "status": "pending", "time": "2023-01-21T20:13:38.188286Z" }, { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-21T20:13:38.186831Z" } } ``` ```json Transient theme={null} { "data": { "id": "6e4d4047-db14-4c09-b238-1215aee50d03", "amount": { "amount": "1.00", "currency": "USD" }, "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" } ], "paymentIds": [], "refundIds": [], "timeline": [ { "status": "pending", "time": "2022-07-21T20:13:38.188286Z" }, { "status": "created", "time": "2022-07-21T20:13:35.579331Z" } ], "createDate": "2022-07-21T20:13:35.578678Z", "updateDate": "2022-07-21T20:13:38.186831Z", "expiresOn": "2022-07-21T21:13:38.087275Z" } } ``` ## Step 3: Have the customer pay onchain Display the deposit address and have the customer send USDC or EURC on the chosen chain. **Expected result:** an onchain transfer to the deposit address is submitted (you confirm settlement in Step 4). ### 3.1. Display the deposit address Give the customer the deposit address, the stablecoin to send (USDC or EURC), and for transient intents the amount. Plain text or a QR code is fine. **Expected result:** the customer can send funds from their wallet. For transient payment intents, you can display the `expiresOn` time to show the customer how long they have to pay. If the customer does not send funds before `expiresOn`, the payment intent moves to a `complete` status with context `expired`. You cannot reuse an expired intent. Create a new payment intent to retry the checkout. ### 3.2. Customer sends funds onchain The customer sends USDC or EURC to the deposit address on the specified blockchain. **Expected result:** the transfer appears onchain against that address. ## Step 4: Confirm payment completion After Circle confirms the transfer, it updates the payment intent and creates a payment record for that inbound transfer. Confirm the intent and the payment match what you expect using webhooks or polling. **Expected result:** payment intent `timeline` shows `complete` with context `paid`, and the linked payment has `status` `paid`. ### 4.1. Inspect the payment intent Subscribe to `paymentIntents` notifications. After the customer pays, the payload includes an updated `timeline`, `amountPaid`, and `paymentIds`. **Expected result:** `paymentIntent` in the notification includes: * `timeline` with `complete` and context `paid` * `paymentIds` listing the payin payment for this transfer * `amountPaid` reflecting the settled amount Example when the payment intent is complete and paid: ```json Continuous theme={null} { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "notificationType": "paymentIntents", "version": 1, "customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" }, "paymentIntent": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "1.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }, { "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" } ], "paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"], "refundIds": [], "timeline": [ { "status": "complete", "context": "paid", "time": "2023-01-21T20:19:24.861094Z" }, { "status": "pending", "time": "2023-01-21T20:13:38.188286Z" }, { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-21T20:19:24.859052Z" } } ``` ```json Transient theme={null} { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "notificationType": "paymentIntents", "version": 1, "customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" }, "paymentIntent": { "id": "6e4d4047-db14-4c09-b238-1215aee50d03", "amount": { "amount": "1.00", "currency": "USD" }, "amountPaid": { "amount": "1.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }, { "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" } ], "paymentIds": ["2b8f9d4e-515f-3c3a-a409-8ab99a2e72c7"], "refundIds": [], "timeline": [ { "status": "complete", "context": "paid", "time": "2022-07-21T20:19:24.861094Z" }, { "status": "pending", "time": "2022-07-21T20:13:38.188286Z" }, { "status": "created", "time": "2022-07-21T20:13:35.579331Z" } ], "createDate": "2022-07-21T20:13:35.578678Z", "updateDate": "2022-07-21T20:19:24.859052Z", "expiresOn": "2022-07-21T21:13:38.087275Z" } } ``` Call the [Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent) endpoint on an interval until the newest `timeline` entry has `status` as `complete` and `context` as `paid`, or until `amountPaid` matches what you expect. **Expected result:** the response `data` includes: * `timeline` with `complete` and context `paid` * `paymentIds` listing the payin payment for this transfer * `amountPaid` reflecting the settled amount Example request: ```curl Continuous theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` ```curl Transient theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/6e4d4047-db14-4c09-b238-1215aee50d03' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response when one payin has completed: ```json Continuous theme={null} { "data": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "1.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }, { "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" } ], "paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"], "refundIds": [], "timeline": [ { "status": "complete", "context": "paid", "time": "2023-01-21T20:19:24.861094Z" }, { "status": "pending", "time": "2023-01-21T20:13:38.188286Z" }, { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-21T20:19:24.859052Z" } } ``` ```json Transient theme={null} { "data": { "id": "6e4d4047-db14-4c09-b238-1215aee50d03", "amount": { "amount": "1.00", "currency": "USD" }, "amountPaid": { "amount": "1.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }, { "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" } ], "paymentIds": ["2b8f9d4e-515f-3c3a-a409-8ab99a2e72c7"], "refundIds": [], "timeline": [ { "status": "complete", "context": "paid", "time": "2022-07-21T20:19:24.861094Z" }, { "status": "pending", "time": "2022-07-21T20:13:38.188286Z" }, { "status": "created", "time": "2022-07-21T20:13:35.579331Z" } ], "createDate": "2022-07-21T20:13:35.578678Z", "updateDate": "2022-07-21T20:19:24.859052Z", "expiresOn": "2022-07-21T21:13:38.087275Z" } } ``` For continuous payment intents, the same deposit address can receive multiple transfers. Each settled transfer adds another ID to `paymentIds`. ### 4.2. Inspect the payment for the same transfer The payment record holds the onchain `transactionHash` and the customer's `fromAddresses` for that transfer. Subscribe to `payments` notifications to receive the updated `payment` object when the transfer settles. **Expected result:** `payment` in the notification has `status` `paid`, a populated `transactionHash`, and `fromAddresses` for the sender. ```json JSON theme={null} { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "notificationType": "payments", "version": 1, "customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" }, "payment": { "id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de", "type": "payment", "status": "paid", "amount": { "amount": "1.00", "currency": "USD" }, "fees": { "amount": "0.01", "currency": "USD" }, "createDate": "2023-01-21T20:16:35.092Z", "updateDate": "2023-01-21T20:19:24.719Z", "merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "merchantWalletId": "1000999922", "paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "settlementAmount": { "amount": "1.00", "currency": "USD" }, "fromAddresses": { "chain": "ETH", "addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"] }, "depositAddress": { "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" }, "transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906" } } ``` Poll the [Get a payment](/api-reference/circle-mint/payments/get-payment) endpoint using the ID from `paymentIds` on the [payment intent](#4-1-inspect-the-payment-intent). Stop when that payment `status` is `paid` and, if you check onchain, when `transactionHash` matches the transfer you expect. For continuous payment intents, `paymentIds` can list more than one transfer over time. Make sure you poll the ID for this specific transfer. **Expected result:** `data` has `type` `payment` and `status` `paid`, a populated `transactionHash`, and `fromAddresses` for the sender when the payin has settled. Example request: ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/payments/{id}' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response: ```json JSON theme={null} { "data": { "id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de", "type": "payment", "status": "paid", "amount": { "amount": "1.00", "currency": "USD" }, "fees": { "amount": "0.01", "currency": "USD" }, "createDate": "2023-01-21T20:16:35.092Z", "updateDate": "2023-01-21T20:19:24.719Z", "merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "merchantWalletId": "1000999922", "paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "settlementAmount": { "amount": "1.00", "currency": "USD" }, "fromAddresses": { "chain": "ETH", "addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"] }, "depositAddress": { "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" }, "transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906" } } ``` For blockchains that use a memo or address tag (for example, XLM or HBAR), the `addressTag` field can appear on `depositAddress` in payment payloads. The payment is complete when `status` is `paid`. # Blockchain Confirmations Source: https://developers.circle.com/circle-mint/references/blockchain-confirmations Confirmation requirements and approximate times for each blockchain supported by Circle Mint. ## What are blockchain confirmations? When you submit a transaction to a blockchain, it starts in a pending state. The network must include it in a block and validate it before it counts as confirmed. Each new block added after that makes the transaction harder to reverse. A **confirmation number** is the number of blocks that must follow a transaction's block before it is final. Once the confirmation number is reached, the transaction can't be reversed. ## Why confirmations matter Without enough confirmations, transactions are at risk of reorganizations (reorgs). A reorg happens when validators discard recent blocks and replace them with new ones, rewriting part of the blockchain's history. This can reverse transactions that appeared settled. Each extra confirmation makes a reorg less likely. Because blockchains differ in design, block times, and consensus, the number of confirmations needed varies by blockchain. ## Confirmation numbers Confirmations show how safe a transaction is from reorg. The confirmation number is how many blocks must be added until a block is considered permanent. Each blockchain's confirmation number is different, and determined by Circle. Confirmation numbers are based on a variety of factors, including the blockchain's history, potential for reorg, and overall network architecture. For layer-2 (L2) blockchains that settle transactions on a separate base layer, Circle waits for blocks on the layer-1 (L1) base blockchain. This happens after the L2 block gets included in an L1 block. | Chain | Confirmations | Approximate time | | ------------------ | ----------------- | ------------------ | | Algorand | **1** | \~3 seconds | | Aptos | **1** | \~500 milliseconds | | Arbitrum | **12 ETH Blocks** | \~4 to 6 minutes | | Avalanche C-Chain | **1** | \~2 seconds | | Base | **12 ETH Blocks** | \~3 to 9 minutes | | Celo | **12 ETH Blocks** | \~3 to 9 minutes | | Codex | **12 ETH Blocks** | \~3 to 9 minutes | | EDGE | **12 ETH Blocks** | \~3 to 5 minutes | | Ethereum | **12** | \~3 minutes | | Hedera | N/A | \~3 seconds | | HyperEVM | **1** | under 1 second | | Injective | **1** | under 1 second | | Ink | **12 ETH Blocks** | \~3 to 9 minutes | | Linea | **65 ETH Blocks** | \~6 to 32 hours | | Monad | **4** | \~1.6 seconds | | Morph | **64** | \~20 to 30 minutes | | NEAR | **1** | \~2 seconds | | Noble | **1** | \~1.53 seconds | | OP Mainnet | **12 ETH Blocks** | \~3 to 9 minutes | | Pharos | **1** | \~20 seconds | | Plume | **12 ETH Blocks** | \~3 to 9 minutes | | Polkadot Asset Hub | **1** | \~12 seconds | | Polygon PoS | **2-3** | \~8 seconds | | Sei | **1** | \~400 milliseconds | | Solana | **1** | \~400 milliseconds | | Sonic | **1** | \~1 second | | Starknet | **65 ETH Blocks** | \~4 to 8 hours | | Stellar | **1** | \~5 seconds | | Sui | **1** | \~500 milliseconds | | Unichain | **12 ETH Blocks** | \~3 to 9 minutes | | World Chain | **12 ETH Blocks** | \~3 to 9 minutes | | XDC | **3** | \~6 seconds | | XRPL | **1** | \~5 seconds | | ZKsync Era | **65 ETH Blocks** | \~5 to 7 hours | **Hedera**: Hedera is built on a hashgraph, not a blockchain. As such, there isn't a count of confirmations before Circle considers a transfer valid. This determination is performed on Hedera directly and is then shared back to Circle. See [Hedera consensus](https://hedera.com/how-it-works) to learn more. **Linea**: Linea requires 65 ETH block confirmations. However, Linea only posts batches to Ethereum every 6-32 hours, so the approximate confirmation time reflects this batch posting interval rather than the ETH block time alone. ## Transfer status When an incoming transfer is included in a block, the API makes it available for you. However, the transfer remains in the `running` status. It won't credit the balance of the associated wallet with the transfer amount until the required number of confirmations has been reached. Once the confirmation number is reached, the transfer status changes to `completed`. If you subscribed to webhook notifications, you receive a message about this change. You can use transfers in your processes before they reach `completed` status. This comes with risk. A blockchain reorganization (reorg) occurs when validators discard recent blocks and replace them with new ones, reversing transactions that appeared settled. If a reorg reverses a transfer you already acted on, the credited balance is rolled back. Reorgs are rare, but if you use transfers before they get enough confirmations, you take on this risk. Waiting for confirmations only applies to onchain transfers. These are transfers where the `source` is type `blockchain`. Transfers between hosted wallets don't need to wait. These transfers have a `source` type of `wallet` and happen instantly. # Sandbox to Production Source: https://developers.circle.com/circle-mint/references/sandbox-and-testing Transition your Circle Mint integration from the sandbox environment to production. Circle provides a sandbox environment at `https://api-sandbox.circle.com` for prototyping and integration testing. Sandbox APIs match production APIs, so you can develop and test without generating real financial transactions. Simulated transactions use [test networks only](/stablecoins/usdc-contract-addresses#testnet) and do not move real funds. For details on idempotent requests, pagination, and date filtering, see the [API reference](/api-reference/idempotent-requests). After completing your integration in the sandbox, follow these steps to move to production. If you have questions, contact your Circle solutions engineer or [customer support](mailto:customer-support@circle.com). Update all API base URLs from `https://api-sandbox.circle.com` to `https://api.circle.com`. Use the [Mint Console](https://app.circle.com/) to create a production API key. Production API keys allow you to work with real funds. Treat production keys with appropriate security protocols. Consult your security team for key management best practices. If you configure an IP allowlist, Circle only accepts requests using your production API key that originate from an IP address on that list. Verify that the static addresses in your system match the ones you registered with Circle. In the sandbox, your API key provides access to all endpoints. In production, access is restricted to the APIs your entity is authorized to use. * Test each API call to confirm it works in production. For example, verify that your [list all balances](/api-reference/circle-mint/account/list-business-balances) call returns results. * If you receive `403` responses or need additional capabilities, contact your Circle representative. Sandbox settlement times are kept short for testing convenience. Production settlement times reflect real-world processing and are longer. For onchain transfers, the time to finality depends on the number of [blockchain confirmations](/circle-mint/references/blockchain-confirmations) required for each blockchain. * Test actual settlement times in production. * Decide whether to perform actions (such as releasing goods) after confirmation or after settlement. Sandbox fees may not reflect your client agreement with Circle. * Review your contract with Circle to understand production fees. * Test transactions in production to determine actual fees. * Update your interface if you pass fees along to your customers. # Supported Chains and Currencies Source: https://developers.circle.com/circle-mint/references/supported-chains-and-currencies Blockchains and currencies supported by Circle Mint and related APIs. **Unsupported assets** Circle Mint and Circle APIs only support USDC and EURC tokens on the indicated blockchains. Don't send unsupported tokens such as USDT or bridged USDC to your Circle Mint address. Doing so might result in a loss of funds. **Cosmos appchains** Circle Mint and Circle APIs only support USDC from Noble. If you transfer USDC from Noble to other appchains via IBC (Inter-Blockchain Communication), you must transfer it back to Noble before you transfer it to your Circle Mint address. Don't attempt to deposit USDC from an appchain other than Noble to your Circle Mint address. Doing so might result in a loss of funds. **Polkadot parachains** Circle Mint and Circle APIs only support USDC from Polkadot Asset Hub. If you transfer USDC from Polkadot Asset Hub to other parachains via XCM, you must transfer it back to Polkadot Asset Hub before you transfer it to your Circle Mint address. Don't attempt to deposit XCM-transferred USDC from a parachain other than Polkadot Asset Hub to your Circle Mint address. Doing so might result in a loss of funds. **Injective** Circle Mint only supports USDC deposits on Injective through the EVM layer. Cosmos-layer USDC deposits are not supported. If you deposit USDC through the Cosmos layer, your funds are stuck and not credited to your Circle Mint account. To recover stuck funds, contact [Circle Support](https://help.circle.com/s/submit-ticket). ## USDC | Chain | API Chain Code | API Currency Code | | ------------------ | -------------- | ----------------- | | Algorand | `ALGO` | `USD` | | Aptos | `APTOS` | `USD` | | Arbitrum | `ARB` | `USD` | | Avalanche C-Chain | `AVAX` | `USD` | | Base | `BASE` | `USD` | | Celo | `CELO` | `USD` | | Codex | `CODEX` | `USD` | | EDGE | `EDGE` | `USD` | | Ethereum | `ETH` | `USD` | | Hedera | `HBAR` | `USD` | | HyperEVM | `HYPEREVM` | `USD` | | Injective | `INJECTIVE` | `USD` | | Ink | `INK` | `USD` | | Linea | `LINEA` | `USD` | | Monad | `MONAD` | `USD` | | Morph | `MORPH` | `USD` | | NEAR | `NEAR` | `USD` | | Noble | `NOBLE` | `USD` | | OP Mainnet | `OP` | `USD` | | Pharos | `PHAROS` | `USD` | | Plume | `PLUME` | `USD` | | Polkadot Asset Hub | `PAH` | `USD` | | Polygon PoS | `POLY` | `USD` | | Sei | `SEI` | `USD` | | Solana | `SOL` | `USD` | | Sonic | `SONIC` | `USD` | | Starknet | `STRK` | `USD` | | Stellar | `XLM` | `USD` | | Sui | `SUI` | `USD` | | Unichain | `UNI` | `USD` | | World Chain | `WORLDCHAIN` | `USD` | | XDC | `XDC` | `USD` | | XRPL | `XRP` | `USD` | | ZKsync Era | `ZKS` | `USD` | ## EURC | Chain | API Chain Code | API Currency Code | | ----------------- | -------------- | ----------------- | | Avalanche C-Chain | `AVAX` | `EUR` | | Base | `BASE` | `EUR` | | Ethereum | `ETH` | `EUR` | | Solana | `SOL` | `EUR` | | Stellar | `XLM` | `EUR` | | World Chain | `WORLDCHAIN` | `EUR` | ## Crypto Deposits and Payouts APIs The Crypto Deposits API and Crypto Payouts API support a subset of blockchains that have USDC available. The following table outlines which blockchain each API supports. | Chain | Crypto Deposits API | Crypto Payouts API | | ------------------ | ------------------- | ------------------ | | Algorand | ✓ | ✓ | | Aptos | ✗ | ✗ | | Arbitrum | ✓ | ✓ | | Avalanche C-Chain | ✓ | ✓ | | Base | ✓ | ✓ | | Celo | ✗ | ✗ | | Codex | ✓ | ✓ | | EDGE | ✓ | ✓ | | Ethereum | ✓ | ✓ | | Hedera | ✓ | ✓ | | HyperEVM | ✓ | ✓ | | Injective | ✓ | ✓ | | Ink | ✓ | ✓ | | Linea | ✓ | ✓ | | Monad | ✓ | ✓ | | Morph | ✓ | ✓ | | NEAR | ✗ | ✗ | | Noble | ✓ | ✓ | | OP Mainnet | ✓ | ✓ | | Pharos | ✓ | ✓ | | Plume | ✓ | ✓ | | Polkadot Asset Hub | ✗ | ✗ | | Polygon PoS | ✓ | ✓ | | Sei | ✓ | ✓ | | Solana | ✓ | ✓ | | Sonic | ✓ | ✓ | | Starknet | ✓ | ✓ | | Stellar | ✓ | ✓ | | Sui | ✗ | ✗ | | Unichain | ✗ | ✗ | | World Chain | ✓ | ✓ | | XDC | ✓ | ✓ | | XRPL | ✓ | ✓ | | ZKsync Era | ✗ | ✗ | ## Using chains and currencies in the API Any time you refer to a currency in a Circle Mint API call, you use a currency and chain pair. For example, to [create a USDC transfer](/api-reference/circle-mint/account/create-business-transfer) on Ethereum, specify the `USD` currency on the `ETH` chain. When referring to balances, you only need to refer to the currency because the value of the currency for Circle-hosted assets is independent of the chain. # Supported Countries Source: https://developers.circle.com/circle-mint/references/supported-countries Countries that support wire transfers for minting and redeeming USDC. ## Wire transfers for minting and redeeming USDC The Circle Core API supports wire transfers from and to bank accounts domiciled in the following countries: ### Asia * Armenia * Azerbaijan * Bahrain * Bangladesh * Bhutan * Bonaire, Sint Eustatius and Saba * Brunei Darussalam * Cambodia * Georgia * Hong Kong * Indonesia * Israel * Japan * Jordan * Kazakhstan * Kuwait * Kyrgyzstan * Malaysia * Mongolia * Oman * Philippines * Republic of Korea * Saudi Arabia * Singapore * Sri Lanka * Taiwan * Tajikistan * Thailand * Timor-Leste * Turkey * United Arab Emirates * Uzbekistan * Vietnam ### Africa * Angola * Benin * Botswana * British Indian Ocean Territory * Burkina Faso * Burundi * Cameroon * Chad * Egypt * Ethiopia * The French Southern Territories * The Gambia * Ghana * Kenya * Madagascar * Malawi * Mauritius * Mayotte * Morocco * Mozambique * Namibia * Niger * Réunion * Rwanda * Saint Helena, Ascension and Tristan da Cunha * Senegal * Seychelles * South Africa * Tanzania * Tunisia * Western Sahara * Zambia * Zimbabwe ### Europe * Åland Islands * Andorra * Austria * Belgium * Bosnia and Herzegovina * Bulgaria * Croatia * Cyprus * Czechia * Denmark * Estonia * The Faroe Islands * Finland * France * Germany * Gibraltar * Greece * Guernsey * Holy See (Vatican City State) * Hungary * Iceland * Ireland * Isle of Man * Italy * Jersey * Latvia * Liechtenstein * Lithuania * Luxembourg * Malta * The Republic of Moldova * Monaco * Montenegro * Republic of North Macedonia * Netherlands * Norway * Poland * Portugal * Romania * San Marino * Serbia * Slovakia * Slovenia * Spain * Svalbard and Jan Mayen * Sweden * Switzerland * United Kingdom ### North America * Anguilla * Antigua and Barbuda * Aruba * The Bahamas * Belize * Bermuda * British Virgin Islands * Canada * Cayman Islands * Costa Rica * Curaçao * Dominica * The Dominican Republic * El Salvador * Greenland * Grenada * Guadeloupe * Honduras * Martinique * Mexico * Montserrat * Puerto Rico * Saint Barthelemy * Saint Lucia * Saint Martin (French part) * Saint Pierre and Miquelon * Saint Vincent and the Grenadines * Sint Maarten (Dutch part) * The Turks and Caicos Islands * United States (excluding HI and NY) * United States Minor Outlying Islands * U.S. Virgin Islands ### South America * Argentina * Bouvet Island * Brazil * Chile * Colombia * Ecuador * French Guiana * Guatemala * Guyana * Paraguay * Peru * The Falkland Islands (Malvinas) * South Georgia and the South Sandwich Islands * Uruguay ### Oceania * American Samoa * Australia * Christmas Island * The Cocos (Keeling) Islands * The Cook Islands * Federated States of Micronesia * Fiji * French Polynesia * Guam * Heard Island and McDonald Islands * Kiribati * The Marshall Islands * Nauru * New Caledonia * New Zealand * Niue * Norfolk Island * The Northern Mariana Islands * Palau * Pitcairn * Samoa * Tokelau * Tuvalu * Wallis and Futuna ### Antarctica * Antarctica This list is subject to change as Circle adds banking partners. Contact the [Customer Care team](https://support.circle.com/) for the latest information. # Quickstart: Refund a Stablecoin Payin Source: https://developers.circle.com/circle-mint/refund-stablecoin-payin Start full or partial stablecoin payin refunds with the Crypto Deposits API. Use the Crypto Deposits API to refund USDC or EURC after a completed [payin](/circle-mint/how-stablecoin-payins-and-payouts-work#refunds), which is a stablecoin deposit from an external wallet into your Circle Mint account. The examples use a continuous [payment intent](/circle-mint/how-stablecoin-payins-and-payouts-work#continuous-and-transient-modes) (open-ended checkout), but the same steps apply to transient intents (single checkout with a fixed amount). Complete [Receive a Stablecoin Payin](/circle-mint/receive-stablecoin-payin) first if you have not accepted a payin yet. ## Prerequisites Before you begin this tutorial, ensure you've: * Obtained a [Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with access to the Crypto Deposits API (stablecoin payins). * [Confirmed a completed stablecoin payin](/circle-mint/receive-stablecoin-payin#step-4-confirm-payment-completion) on the payment intent you want to refund. * Confirmed there is no pending payment on that intent. * Installed cURL for API calls. ## Step 1: Start a refund on the payment intent Call the [Refund a payment intent](/api-reference/circle-mint/payments/refund-payment-intent) endpoint. * Partial refund: set `toAmount` to the amount you return to the customer. You can send multiple partial refunds up to what was settled. * Full refund: use the request fields for refunding the remaining balance as described in the API reference. **Expected result:** a `refund` object with `status` `pending` and a refund `id`. Example request: ```bash cURL theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b/refund' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "9aed1aab-292a-427f-aae1-e0e358fef1c9", "destination": { "chain": "ETH", "address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69" }, "amount": { "currency": "USD" }, "toAmount": { "amount": "0.50", "currency": "USD" } }' ``` Example response: ```json JSON theme={null} { "data": { "id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6", "type": "refund", "status": "pending", "amount": { "currency": "USD" }, "createDate": "2023-01-22T15:29:58.000000Z", "updateDate": "2023-01-22T15:29:58.000000Z", "merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "merchantWalletId": "1000999922", "paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "settlementAmount": { "amount": "0.50", "currency": "USD" }, "depositAddress": { "chain": "ETH", "address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69" } } } ``` ## Step 2: Inspect the payment intent Call [Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent) to confirm the refund is reflected on the intent. **Expected result:** the response includes these fields: * `amountRefunded`: cumulative amount refunded on this intent so far. * `refundIds`: refund IDs associated with this intent. * `paymentIds`: IDs of payin payments on this intent. * `timeline`: includes a `refunded` entry after the refund is recorded (alongside earlier statuses such as `paid`). Example request: ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response: ```json JSON theme={null} { "data": { "id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "currency": "USD", "settlementCurrency": "USD", "amountPaid": { "amount": "1.00", "currency": "USD" }, "amountRefunded": { "amount": "0.50", "currency": "USD" }, "paymentMethods": [ { "type": "blockchain", "chain": "ETH", "address": "0x97de855690955e0da79ce5c1b6804847e7070c7f" } ], "fees": [ { "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }, { "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" } ], "paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"], "refundIds": ["3389f4ba-aafd-4eef-aaa2-3292df8f62e6"], "timeline": [ { "status": "refunded", "time": "2023-01-22T15:30:00.000000Z" }, { "status": "complete", "context": "paid", "time": "2023-01-21T20:19:24.861094Z" }, { "status": "pending", "time": "2023-01-21T20:13:38.188286Z" }, { "status": "created", "time": "2023-01-21T20:13:35.579331Z" } ], "type": "continuous", "createDate": "2023-01-21T20:13:35.578678Z", "updateDate": "2023-01-22T15:30:00.000000Z" } } ``` ## Step 3: Confirm refund completion Call [Get a payment](/api-reference/circle-mint/payments/get-payment) with the original payin payment `id` or with the refund `id` from [Step 1](#step-1-start-a-refund-on-the-payment-intent). **Expected result:** the response has `type` `refund` and `status` `paid` when the refund has settled onchain. If `status` is still `pending`, the refund has not finished. Poll [Get a payment](/api-reference/circle-mint/payments/get-payment) on an interval, or subscribe to `payments` notifications (see the [notifications quickstart](/circle-mint/circle-apis-notifications-quickstart)) to learn when the status changes. Example request: (replace the ID with the payment or refund `id`) ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/payments/3389f4ba-aafd-4eef-aaa2-3292df8f62e6' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response: ```json JSON theme={null} { "data": { "id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6", "type": "refund", "status": "paid", "amount": { "currency": "USD" }, "fees": { "amount": "0.01", "currency": "USD" }, "createDate": "2023-01-22T15:29:58.000000Z", "updateDate": "2023-01-22T15:35:12.000000Z", "merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522", "merchantWalletId": "1000999922", "paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b", "settlementAmount": { "amount": "0.50", "currency": "USD" }, "fromAddresses": { "chain": "ETH", "addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"] }, "depositAddress": { "chain": "ETH", "address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69" }, "transactionHash": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678" } } ``` # Quickstart: Send a Stablecoin Payout Source: https://developers.circle.com/circle-mint/send-stablecoin-payout Create an address book recipient, send USDC or EURC onchain, and confirm payout completion with the Crypto Payouts API. Use the Crypto Payouts API to send onchain USDC or EURC [stablecoin payouts](/circle-mint/how-stablecoin-payins-and-payouts-work) from Circle Mint to third-party blockchain addresses. You create a recipient, wait until it is active, create a payout, then confirm completion using the API or webhooks. The examples in this quickstart use Ethereum, but you can use any [supported blockchain](/circle-mint/references/supported-chains-and-currencies). Stablecoin payouts are for transferring funds to third-party recipients such as vendors, contractors, remittances, or customers. If you want to transfer funds to a wallet your business owns or controls, see the [Transfer USDC Onchain](/circle-mint/howtos/transfer-on-chain) guide. ## Prerequisites Before you begin this tutorial, ensure you've: * Obtained a [Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with access to the Crypto Payouts API (stablecoin payouts). * Obtained a source wallet ID (`source.id`) with funds available for payouts. * Installed cURL for API calls. * (Optional) Configured [webhook notifications](/circle-mint/circle-apis-notifications-quickstart). ## Step 1: Create an address book recipient Add a recipient in your Mint address book before any payout. If delayed withdrawals are on in the Circle Mint console, new recipients stay `inactive` for 24 hours before they become `active`. If delayed withdrawals are off, recipients become `active` within seconds. Toggle **delayed withdrawals** under your Mint account settings in the Circle Mint console. ### 1.1. Create a recipient Call [Create address book recipient](/api-reference/circle-mint/payouts/create-address-book-recipient). **Expected result:** a recipient `id` (status may be `pending` until active). Example request: ```bash cURL theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/addressBook/recipients' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde", "chain": "ETH", "address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3", "metadata": { "email": "satoshi@circle.com", "bns": "testbns", "nickname": "test nickname desc" } }' ``` Example response: ```json JSON theme={null} { "data": { "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe", "chain": "ETH", "address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3", "metadata": { "nickname": "test nickname desc", "email": "satoshi@circle.com", "bns": "testbns" }, "status": "pending", "updateDate": "2022-09-22T14:16:34.985353Z", "createDate": "2022-09-22T14:16:34.985353Z" } } ``` ### 1.2. Confirm recipient status The recipient must be `active` before you create a payout. Use webhooks or poll [Get address book recipient](/api-reference/circle-mint/payouts/get-address-book-recipient). If delayed withdrawals are on, wait until status moves from `inactive` to `active`. **Expected result:** `status` is `active`. After you [subscribe to notifications](/circle-mint/circle-apis-notifications-quickstart) for `addressBookRecipients`, Circle sends updates when a recipient is created or its status changes. Example `addressBookRecipients` notification when the recipient becomes active: ```json JSON theme={null} { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802", "notificationType": "addressBookRecipients", "version": 1, "customAttributes": { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802" }, "addressBookRecipient": { "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe", "chain": "ETH", "address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3", "metadata": { "nickname": "test nickname desc", "email": "satoshi@circle.com", "bns": "testbns" }, "status": "active", "updateDate": "2022-09-22T14:16:34.985353Z", "createDate": "2022-09-22T14:16:34.985353Z" } } ``` Call the [Get address book recipient](/api-reference/circle-mint/payouts/get-address-book-recipient) endpoint on an interval until the recipient `status` is `active`. Example request: (use the `id` from the [create recipient](#1-1-create-a-recipient) response) ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/addressBook/recipients/dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response when the recipient is active: ```json JSON theme={null} { "data": { "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe", "chain": "ETH", "address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3", "metadata": { "nickname": "test nickname desc", "email": "satoshi@circle.com", "bns": "testbns" }, "status": "active", "updateDate": "2022-09-22T14:16:34.985353Z", "createDate": "2022-09-22T14:16:34.985353Z" } } ``` Do not continue until the recipient `status` is `active`. ## Step 2: Create a payout Call [Create payout](/api-reference/circle-mint/payouts/create-payout) with the recipient `id` from [Create a recipient](#1-1-create-a-recipient), your source wallet, and amounts. If `toAmount.currency` is omitted, `amount.currency` is used as the receiving currency. **Expected result:** a payout `id` with `status` `pending`. `source.identities` is required for payouts of 3,000 USD or more. It describes the sender (your organization) for FinCEN [Travel Rule](/circle-mint/howtos/transfer-on-chain#travel-rule-compliance) compliance. Example request: ```bash cURL theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/payouts' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "source": { "type": "wallet", "id": "12345", "identities": [ { "type": "individual", "name": "Satoshi Nakamoto", "addresses": [ { "line1": "100 Money Street", "line2": "Suite 1", "city": "Boston", "district": "MA", "postalCode": "01234", "country": "US" } ] } ] }, "destination": { "type": "address_book", "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe" }, "amount": { "amount": "3000.14", "currency": "USD" }, "toAmount": { "currency": "USD" } }' ``` Example response: ```json JSON theme={null} { "data": { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "sourceWalletId": "12345", "destination": { "type": "address_book", "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe" }, "amount": { "amount": "3000.14", "currency": "USD" }, "toAmount": { "amount": "3000.14", "currency": "USD" }, "status": "pending", "updateDate": "2020-04-10T02:13:30.000Z", "createDate": "2020-04-10T02:13:30.000Z" } } ``` If [Get payout](/api-reference/circle-mint/payouts/get-payout) later returns `status` `failed`, check the payload for an `errorCode` and related fields (for example insufficient balance or an invalid recipient). Retry only after you fix the underlying issue. ## Step 3: Confirm payout completion Confirm the payout using webhooks or by polling [Get payout](/api-reference/circle-mint/payouts/get-payout). **Expected result:** `status` is `complete` when funds are sent and delivered onchain. Subscribe to `payout` notifications to receive updates as status changes. Each message includes the payout object with the latest `status`. Example `payout` notification when the `status` is `complete`: ```json JSON theme={null} { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802", "notificationType": "payout", "version": 1, "customAttributes": { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802" }, "payout": { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "sourceWalletId": "12345", "destination": { "type": "address_book", "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe" }, "amount": { "amount": "3000.14", "currency": "USD" }, "toAmount": { "amount": "3000.14", "currency": "USD" }, "fees": { "amount": "0.00", "currency": "USD" }, "networkFees": { "amount": "0.30", "currency": "USD" }, "status": "complete", "createDate": "2020-04-10T02:13:30.000Z", "updateDate": "2020-04-10T02:13:30.000Z" } } ``` Call the [Get payout](/api-reference/circle-mint/payouts/get-payout) endpoint on an interval until `status` is `complete`. Example request: (use the payout `id` from the [create payout](#step-2-create-a-payout) response) ```bash cURL theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29' \ --header 'Accept: application/json' \ --header 'X-Request-Id: ${GUID}' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` Example response when the payout is complete: ```json JSON theme={null} { "data": { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "sourceWalletId": "12345", "destination": { "type": "address_book", "id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe" }, "amount": { "amount": "3000.14", "currency": "USD" }, "toAmount": { "amount": "3000.14", "currency": "USD" }, "fees": { "amount": "0.00", "currency": "USD" }, "networkFees": { "amount": "0.30", "currency": "USD" }, "status": "complete", "createDate": "2020-04-10T02:13:30.000Z", "updateDate": "2020-04-10T02:13:30.000Z" } } ``` # Quickstart: Swap Between USDC and EURC Source: https://developers.circle.com/circle-mint/swap-between-usdc-and-eurc The Cross-Currency API lets you swap between USDC and EURC. This guide walks you through how to use the Cross-Currency API to obtain a quote for a EURC to USDC exchange and then execute the swap. ## Prerequisites Before you begin this quickstart, ensure that you have: * Obtained an API key for Mint from Circle * Obtained access to the Cross-Currency API * cURL installed on your development machine * Funded your Circle Mint account with EURC This quickstart provides API requests in cURL format, along with example responses. ## Part 1: Request a quote Using a UUIDv4 generator, generate a UUID to use as the idempotency key. Using the idempotency key, request a quote from the [`/exchange/quotes`](/api-reference/circle-mint/cross-currency/get-quote) endpoint for exchanging a specific amount of EURC to USDC. You must include an `amount` field on either the `from` or the `to` object, but not both. The `type` field must be set to `tradable` to get a locked rate quote. The following is an example request for a quote: **Note:** Quotes are valid for 3 seconds and must be refreshed if not used in that time frame. ```shell Shell theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/quotes' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "tradable", "idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b", "from": { "currency": "EURC", "amount": 100 }, "to": { "currency": "USDC", "amount": null } } ' ``` **Response** ```json JSON theme={null} { "data": { "id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1", "rate": 0.1974, "from": { "currency": "EURC", "amount": 100.0 }, "to": { "currency": "USDC", "amount": 110.0 }, "expiry": "2023-10-26T14:37:20.804786Z", "type": "tradable" } } ``` ## Part 2: Initiate the trade Generate another idempotency key, and then use it to confirm the quote with the [`/exchange/trades`](/api-reference/circle-mint/cross-currency/create-fx-trade) endpoint and initiate the trade. This locks in the rate. Funds are debited from your Circle Mint account in accordance with your settlement schedule. The following is an example request: ```shell Shell theme={null} curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/trades' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' \ --header 'Content-Type: application/json' \ --data-raw '{ "idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } ' ``` **Response** ```json JSON theme={null} { "data": { "id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1", "from": { "currency": "EURC", "amount": 100.0 }, "to": { "currency": "USDC", "amount": 110.0 }, "status": "pending", "createDate": "2023-10-26T14:37:20.804786Z", "updateDate": "2023-10-26T14:37:20.804786Z", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } } ``` ## Part 3: Get the settlement batch (optional) Once you have initiated the trade, you can optionally retrieve the settlement batch from the [`/exchange/trades/settlements`](/api-reference/circle-mint/cross-currency/get-settlements) endpoint on your account and review the details of the settlement. ```shell Shell theme={null} curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ${YOUR_API_KEY}' ``` **Response** ```json JSON theme={null} { "data": [ { "id": "7bf8cf16-2dc1-4514-9b64-c471561a7321", "entityId": "2bcca31e-784f-4b21-9002-8239551e985f", "status": "settled", "createDate": "2025-05-27T19:00:48.688390Z", "updateDate": "2025-05-27T19:00:51.619761Z", "details": [ { "id": "454fc27e-ae8a-461f-834b-d19d8dec481e", "type": "receivable", "status": "completed", "amount": { "currency": "EURC", "amount": "1.45" }, "createDate": "2025-05-27T19:00:48.688333Z", "updateDate": "2025-05-27T19:00:51.617187Z" }, { "id": "d22eebbc-0db9-4818-9f3a-24b39d02524a", "type": "payable", "status": "completed", "amount": { "currency": "USDC", "amount": "1.60" }, "expectedPaymentDueAt": "2025-05-27T22:00:00Z", "createDate": "2025-05-27T19:00:48.688369Z", "updateDate": "2025-05-27T19:00:51.617187Z" } ] }, { "id": "e8edbad2-d0a3-4560-b892-d035ca26ba69", "entityId": "2bcca31e-784f-4b21-9002-8239551e985f", "status": "settled", "createDate": "2025-05-19T22:10:29.945750Z", "updateDate": "2025-05-20T16:52:01.754271Z", "details": [ { "id": "b19a182d-ac3d-436b-be4b-5f8b549c74fd", "type": "payable", "status": "completed", "amount": { "currency": "USDC", "amount": "1.60" }, "expectedPaymentDueAt": "2025-05-19T22:00:00Z", "createDate": "2025-05-19T22:10:29.944724Z", "updateDate": "2025-05-20T16:52:01.750552Z" }, { "id": "6644a7e5-4793-48ef-882b-742cf7bea4ee", "type": "receivable", "status": "completed", "amount": { "currency": "EURC", "amount": "1.45" }, "createDate": "2025-05-19T22:10:29.944441Z", "updateDate": "2025-05-20T16:52:01.750552Z" } ] } ] } ``` # Manage Webhook Subscriptions Source: https://developers.circle.com/circle-mint/webhook-subscription-management Subscribe to webhook notifications to be notified automatically when a transaction occurs. *Circle's webhooks are an automated method for apps to receive notifications the moment a transaction is completed or fails to complete. Circle will make a request to your application when an operation is completed. That means your app doesn't need to poll Circle to know whether an operation has completed in the blockchain—the confirmation will arrive automatically.* For configuring a notification subscriber endpoint, see the [Notifications Quickstart](/circle-mint/circle-apis-notifications-quickstart). ## Set Up Subscriptions Through the Circle UI in Sandbox or Production, you can set up webhook subscriptions. This can be done by: 1. Navigating to Developer → Subscriptions in Sandbox or Production. 2. Selecting Add Subscription 3. Entering an endpoint URL you would like notifications to go to. 4. Selecting Add Endpoint **Automatic subscription removal** All subscriptions in sandbox are removed after 30 days. ## View Subscriptions Via Circle's UI you can view all subscriptions registered to your account on Developer → Subscriptions. Circle's Sandbox environment permits up to three subscriptions and the Production environment supports one. ## Remove Subscriptions While viewing subscriptions you are also provided the option to remove subscriptions. This is done by simply selecting kebab on the Endpoint and selecting remove. ## API Endpoints All these actions can also be done using Circle's APIs. See [API reference subscriptions](/api-reference/circle-mint/general/create-subscription) for more detail. # Contracts Source: https://developers.circle.com/contracts Circle Contracts empowers developers to utilize smart contracts within their applications. Contracts facilitates the creation, deployment, and execution of smart contracts through an intuitive Developer Console or APIs, ensuring flexibility and ease of integration. The goal is to simplify the process of leveraging smart contracts for real-world use cases in applications. Some examples of what developers can do using Contracts: 1. **Deploying Custom Contracts:** Bring your custom contracts to life by efficiently deploying them onto the blockchain, expanding the functionality and capabilities of your application. 2. **Deploying NFT Contracts:** Easily deploy NFT contracts and programmatically mint unique tokens for end-users, enabling the creation of digital assets and collectibles. 3. **Creating On-Chain Loyalty Programs:** Establish on-chain loyalty programs within your applications, allowing users to earn and redeem rewards seamlessly. 4. **Interacting with DeFi Projects:** Effortlessly integrate with popular DeFi projects like Uniswap, enabling users to interact with decentralized exchanges and other financial services with just a few clicks. 5. **Integrating Circle Contracts:** Seamlessly incorporate Circle contracts, such as [CCTP](/cctp/evm-smart-contracts)'s, into your application, providing secure and efficient transfer functionalities. 6. **Monitor Your Smart Contracts:** Set up push notifications for any events occurring in your smart contracts. **No-code & API support** Contracts can be accessed in two ways by developers: 1. **Console:** You can explore, interact and manage smart contracts without writing any code. This is ideal for non-technical users, or any low frequency use. The console supports exploring contracts (view functions, events, transactions), deploying contracts (via templates) and managing the contract (any read/write function on the contract). 2. **SDK/API:** You can also receive the benefits of Contracts programmatically! Combining with developer-controlled wallets and Gas Station, you can deploy, interact and manage smart contracts at scale via simple APIs - in a gasless fashion! The APIs that we provide include deploying contracts (via bytecode or templates), read or write contract executions. ## Explore smart contracts The Developer Services console allows developers to view the details of any smart contract. Developers can import a smart contract by adding its address and blockchain. Once imported, developers can explore the contract, view the ABI functions, read the source code, see all transactions, subscribe to events, or execute function calls. ## Deploy smart contracts Developers can deploy smart contracts using Circle Contracts by writing their contracts or using pre-vetted templates provided by Circle. To deploy a contract, developers need to create a Developer-controlled wallet using Circle Wallets and use it to deploy the contract across any supported chains. Deployment can be done through the console in a no-code way or programmatically using APIs. ### Deploy a custom contract If you have already written a smart contract on an IDE, you can deploy it by providing the compiled bytecode and ABI. For console deployment, create a console wallet (a smart contract wallet) and use it to deploy the contract on the desired chain. Include the source code, ABI, and bytecode for API deployment in the request parameters. ### Deploy contracts with templates For developers unfamiliar with smart contract engineering, Templates provide code snippets to deploy contracts without writing any solidity code. These templates, curated by the Circle team and audited by third-party auditors, cover popular onchain use cases. Fill in the required properties and deploy the contract. Templates can be deployed through the console or APIs. Some templates that we support include: * Token (ERC-20) contract, by Thirdweb * NFT (ERC-721) contract, by Thirdweb * Multi-Token (ERC-1155) contract, by Thirdweb * Airdrop contract, by Thirdweb ## Manage smart contracts Once deployed, you can use the console to manage your smart contracts. This includes viewing analytics, calling contract admin functions (e.g. changing ownership, updating configs) or subscribing to events. The console provides an easy way to update and manage contracts post-deployment. Developers can access analytics such as transactions and events for contracts deployed using Contracts via APIs. ## Interact with smart contracts Interacting with smart contracts allows you to integrate existing onchain contracts into your applications. Import the contract and explore its various functions. You can add parameters and generate API resources to interact with the contract in a straightforward manner. ## Event Monitoring Event Monitoring allows developers to track onchain events emitted by their smart contracts in real time. By setting up event monitors, you can receive instant notifications whenever specified events occur. This capability enables developers to respond programmatically to important actions within their applications, making it easier to implement automation and enhance user experiences. # Airdrop Template Source: https://developers.circle.com/contracts/airdrop The Airdrop template is an audited, ready-to-deploy, airdrop smart contract for ERC-20, ERC-721, ERC-1155, or native tokens. The airdrop is performed by “pushing tokens,” which happens when the contract owner transfers tokens to the receivers' addresses. Receivers do not need to take any action and do not incur a gas fee. The following are common use cases for the Airdrop template: * **Bulk token distribution**: Automatically distribute tokens to users in bulk. For example, project teams or token issuers can reward their community members, early adopters, and participants in specific campaigns by distributing tokens directly to their blockchain addresses. * **Marketing and promotion**: Increase awareness and engagement with a project or token. When a project distributes tokens to a large number of users, this attracts attention and incentivizes users to explore and participate in the project's ecosystem. * **Community building and engagement**: Foster a strong and active community around a project. Distribute tokens to community members to incentivize them to stay engaged, participate in discussions, provide feedback, and contribute to the overall growth and success of the project. We recommend setting a maximum of 500 recipients per airdrop transaction. More than 500 recipients can cause the blockchain fees to spike or the transaction to fail. ## Deployment parameters The Airdrop template creates a customized airdrop smart contract to distribute ERC-20, ERC-721, ERC-1155, or native tokens to multiple users. To create a contract using this template, provide the following parameter values when deploying a smart contract template. To deploy a template, send a `POST` request to the `/templates/{id}/deploy` endpoint. **Template ID**: 13e322f2-18dc-4f57-8eed-4bddfc50f85e ### Template deployment parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `defaultAdmin` | Add | X | Address of the default admin. This address can execute permissable functions on the contract.

**Important:** You lose administrative access to the contract if this is not set to an address you control. | | `contractURI` | String | | URL for the marketplace metadata of your contract. | ## Common functions This section lists the most commonly used functions on the Airdrop template, their respective parameters, and potential failure scenarios. These functions include: * [setOwner \[write\]](#setowner-write) * [airdropERC1155 \[write\]](#airdroperc1155-write) * [airdropERC721 \[write\]](#airdroperc721-write) * [airdropERC20 \[write\]](#airdroperc20-write) * [owner \[read\]](#owner-read) ## setOwner \[write] The `setOwner` function sets the owner of the smart contract or transfers ownership from the existing owner to a new owner. ### Parameters The name of the parameter is not used in the request body. | Parameter | Type | Description | | :----------- | :------ | :------------------------ | | \_*newOwner* | address | Address of the new owner. | ### Failure scenarios * The `setOwner` function fails if it is called by a non-admin. ## airdropERC1155 \[write] The `airdropERC1155` function allows the owner of this address to send ERC-1155 tokens to a list of recipients. ### Parameters The names of the parameters are not used in the request body. | Parameter | Type | Description | | ------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_*newOwner* | address | Address of the new owner. | | \_*contents* | tuple:

- address
- uint256
- uint256 | Array of recipient information:

- Address that is to receive the airdrop
- ID of the token within the ERC-1155 contract to be distributed
- Amount of the token within the ERC-1155 contract to be distributed | ### Failure scenarios The `airdropERC1155` function fails if: * It is called by a non-admin * It contains incorrect token information, such as an invalid token ID * The Airdrop contract is not approved for what's being transferred, which means it is not authorized to transfer assets on the airdropper's behalf * Airdropping to a contract without a receive function ### Example The following sample code shows the new owner address and contents when sending a `POST` request to the `/developer/transactions/contractExecution` endpoint: ```javascript JSON theme={null} "abiParameters": [ "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF", [ ["0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528", 0, 10], ["0xf4e2B0fcbd0DC4b326d8A52B718A7bb43BdBd072", 0, 10], ] ] ``` ## airdropERC721 \[write] The `airdropERC721` function allows the owner of this address to send ERC-20 tokens to a list of recipients. ### Parameters The names of the parameters are not used in the request body. | Parameter | Type | Description | | ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | \_*tokenAddress* | address | Address of the new owner. | | \_*contents* | tuple:

- address
- uint256 | Array of recipient information:

- Address that is to receive the airdrop
- Amount of the token within the ERC-721 contract to be distributed | ### Failure scenarios The `airdropERC721` function fails if: * It is called by a non-admin * It contains incorrect token information, such as an invalid token ID * The Airdrop contract is not approved for what's being transferred, which means it is not authorized to transfer assets on the airdropper's behalf * Airdropping to a contract without a receive function ### Example The following sample code shows the token address and contents when sending a `POST` request to the `/developer/transactions/contractExecution` endpoint: ```javascript JSON theme={null} "abiParameters": [ "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF", [ ["0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528", 0], ["0xf4e2B0fcbd0DC4b326d8A52B718A7bb43BdBd072", 1], ] ] ``` ## airdropERC20 \[write] The `airdropERC20` function allows the owner of this address to send NFTs to a list of recipients. ### Parameters The names of the parameters are not used in the request body. | Parameter | Type | Description | | ---------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | \_*tokenAddress* | address | Address of the token. | | \_*contents* | tuple:

- address
- uint256 | Array of recipient information:

- Address that is to receive the airdrop
- Quantity of tokens to be transferred | ### Failure scenarios The `airdropERC20` function fails if: * It is called by a non-admin * The Airdrop contract is not approved for what's being transferred, which means it is not authorized to transfer assets on the airdropper's behalf * Airdropping to a contract without a receive function ### Example The following sample code shows the token address and contents when sending a `POST` request to the `/developer/transactions/contractExecution` endpoint: ```javascript JSON theme={null} "abiParameters": [ "0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF", "0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb", [ [ "0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528", 100 ] ] ] ``` ## owner \[read] The `owner` function retrieves the address of the current owner. ### Example The following sample code shows the `owner` function when sending a `POST` request to the `/contracts/query` endpoint: ```javascript JSON theme={null} "abiFunctionSignature": "owner()" ``` # How-to: Create an API Key Source: https://developers.circle.com/contracts/create-api-key Create an API key in the Circle Console to authenticate Smart Contract Platform API requests. ## Overview Create an API key so your server-side applications can authenticate requests to Circle's APIs. You can create and manage API keys in the [Circle Console](https://console.circle.com/). ## Prerequisites Before you begin, ensure you have: * Signed up for a Circle Developer account at [console.circle.com/signup](https://console.circle.com/signup). * Reviewed the [Keys](/build/keys) page for background on key types and authentication. ## Steps ### Step 1. Open the API & Client Keys page Sign in to the [Circle Console](https://console.circle.com/) and select **API & Client Keys** from the left sidebar. ### Step 2. Create a key Select **Create a key**, then choose **API Key**. Enter a name for your API key and select the access level: * **Standard**: grants read/write access to all APIs, including newly introduced endpoints. * **Restricted Access**: limits the key to specific products and permission levels. When you choose restricted access, configure the following options. Select the products the key can access: * **Webhooks**: endpoints for [webhook subscriptions](/api-reference/wallets/common/create-subscription). * **Wallets**: all endpoints for [user-controlled](/api-reference/wallets/user-controlled-wallets/create-user) and developer-controlled wallets. * **Contracts**: endpoints for [smart contracts](/api-reference/contracts/smart-contract-platform/import-contract). Set the permission level for each product: * **No permission**: the key cannot call any endpoints for that product. * **Read**: the key can call read-only GET endpoints. * **Read/Write**: the key can call all endpoints. You can also add IP addresses or ranges to the IP allowlist for additional security. ### Step 3. Copy your API key After the Console confirms that your key was generated, select **Show** to reveal the key value. Copy it and store it securely. You need it to authenticate API requests. # Quickstart: Deploy an ERC-1155 Contract Template Source: https://developers.circle.com/contracts/deploy-smart-contract-template Use Circle Contract Templates to deploy smart contracts without writing Solidity This quickstart walks you through deploying an ERC-1155 Multi-Token contract using Contract Templates and minting your first token. Contract Templates make it easy to integrate smart contracts into your application without writing Solidity code. Deploy contracts in minutes using curated and audited templates that support popular onchain use cases. **Note:** This quickstart provides all the code you need to deploy an ERC-1155 contract and mint tokens. You can deploy using either the [Console](#console-path) or [API](#api-path). ## Prerequisites Before you begin, ensure you have: * A [Circle Developer Account](https://console.circle.com) * For the API path: * An [API key](/contracts/create-api-key) * A [dev-controlled wallet](/wallets/dev-controlled/create-your-first-wallet) * Your [Entity Secret registered](/wallets/dev-controlled/register-entity-secret) ## Evaluate templates To learn more about the ERC-1155 template or other templates, visit: * **[Console](https://console.circle.com):** View templates, their use cases, ABI functions, events, and code. * **[Templates Glossary](/contracts/scp-templates-overview):** Review all templates and their configuration options. *** ## Console path Use the Console and a Console Wallet to deploy a smart contract template and mint a token. This is the preferred method for those new to smart contracts. ### Step 1: Set up your Console Wallet Console Wallets are Smart Contract Accounts designed for use within the Console. They leverage [Gas Station](/wallets/gas-station), eliminating the need to maintain gas for transaction fees. If you don't have a Console Wallet, you'll be prompted to create one during deployment. **Console Wallet Deploy Cost:** Unlike EOAs, SCAs cost gas to deploy. With lazy deployment, you won't pay the gas fee at wallet creation as it's charged when you initiate your first outbound transaction. ### Step 2: Deploy the smart contract In the [Console](https://console.circle.com): 1. Navigate to the **Templates** tab. 2. Select **Multi-Token** ERC-1155. 3. Fill in the deployment parameters: | Parameter | Description | | :------------------------- | :----------------------------------------------------------------------------------------------------- | | **Name** | The offchain name of the contract, only visible in Circle's systems. Use `MyERC1155Contract`. | | **Contract Name** | The onchain name for the contract. Use `MyERC1155Contract`. | | **Default Admin** | The address with admin permissions to execute permissioned functions. Use your Console Wallet address. | | **Primary Sale Recipient** | The address that receives first-time sale proceeds. Use your Console Wallet address. | | **Royalty Recipient** | The address that receives royalties from secondary sales. Use your Console Wallet address. | | **Royalty Percent** | The royalty share as a decimal (for example, `0.05` for 5% of secondary sales). Use `0`. | | **Network** | The blockchain network to deploy onto. Select `Arc Testnet`. | | **Select Wallet** | The wallet to deploy the smart contract from. Select your Console Wallet. | | **Deployment Speed** | The fee level affecting transaction processing speed (FAST, AVERAGE, SLOW). Select `AVERAGE`. | 4. Select **Deploy**. **Console Wallet Creation:** After selecting a network, you'll be prompted to create a Console Wallet. This wallet is automatically created on all available networks. On testnet, a [Gas Station Policy](/wallets/gas-station/policy-management) is also created. Once deployed, you'll return to the **Contracts** dashboard. The deployment status will initially show **Pending**, then change to **Complete** after a few seconds. ### Step 3: Mint a token In the [Console](https://console.circle.com): 1. Navigate to the **Contracts** tab. 2. Select your **MyERC1155Contract**. 3. Select the **ABI Functions** tab → **Write** → **mintTo**. 4. Fill in the parameters: | Parameter | Description | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **\_to** | The wallet address to receive the minted token. Use your Console Wallet address. | | **\_tokenId** | The token ID to mint, identifying the token type in ERC-1155. Use max uint256 (`115792089237316195423570985008687907853269984665640564039457584007913129639935`) to create token ID 0. For subsequent tokens, use `0` for ID 1, `1` for ID 2, etc. | | **\_uri** | The URI for the token metadata, such as an IPFS CID or CDN URL. | | **\_amount** | The quantity of tokens to mint. Use `1`. | 5. Select **Execute Function** → ensure your Console Wallet is selected → **Execute**. Select **View Transaction History** to monitor the transaction. Once the state shows **Complete**, the token has been minted successfully. **Inbound Transaction:** You'll also see an inbound transfer indicating the token was minted to your Console Wallet. *** ## API path Use APIs to deploy a smart contract template and mint a token programmatically. This option requires an API key and a Dev-Controlled Wallet. ### Step 1: Set up your environment #### 1.1. Get your wallet information Retrieve your wallet ID using the [`GET /wallets`](/api-reference/wallets/developer-controlled-wallets/get-wallets) API. Ensure: * Wallet custody type is **Dev-Controlled** * Blockchain is **Arc Testnet** * Account type is **SCA** (recommended—removes need for gas) Note your wallet's address for subsequent steps. #### 1.2. Understand deployment parameters | Parameter | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | | `idempotencyKey` | A unique value for request deduplication. | | `name` | The offchain contract name. Use `MyERC1155Contract`. | | `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. | | `templateId` | The template identifier. Use `aea21da6-0aa2-4971-9a1a-5098842b1248` for ERC-1155. See [Templates](/contracts/scp-templates-overview). | | `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. | | `entitySecretCiphertext` | The re-encrypted entity secret. See [How the Entity Secret Works](/wallets/dev-controlled/entity-secret-management). | | `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. | | `templateParameters` | The onchain initialization parameters (see below). | #### 1.3. Template parameters | Parameter | Description | | :--------------------- | :-------------------------------------------------------------------------------- | | `name` | The onchain contract name. Use `MyERC1155Contract`. | | `defaultAdmin` | The address with admin permissions. Use your Dev-Controlled Wallet address. | | `primarySaleRecipient` | The address for first-time sale proceeds. Use your Dev-Controlled Wallet address. | | `royaltyRecipient` | The address for secondary sale royalties. Use your Dev-Controlled Wallet address. | | `royaltyPercent` | The royalty share as a decimal (for example, `0.05` for 5%). Use `0`. | ### Step 2: Deploy the smart contract Deploy by making a request to [`POST /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template): ```javascript Node.js theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ apiKey: "", entitySecret: "", }); const circleContractSdk = initiateSmartContractPlatformClient({ apiKey: "", entitySecret: "", }); const response = await circleContractSdk.deployContractTemplate({ id: "aea21da6-0aa2-4971-9a1a-5098842b1248", blockchain: "ARC-TESTNET", name: "MyERC1155Contract", walletId: "", templateParameters: { name: "MyERC1155Contract", defaultAdmin: "", primarySaleRecipient: "", royaltyRecipient: "", royaltyPercent: 0, }, fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); ``` ```python Python theme={null} from circle.web3 import utils, developer_controlled_wallets, smart_contract_platform client = utils.init_developer_controlled_wallets_client( api_key="", entity_secret="" ) scpClient = utils.init_smart_contract_platform_client( api_key="", entity_secret="" ) api_instance = smart_contract_platform.TemplatesApi(scpClient) request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({ "blockchain": "ARC-TESTNET", "name": "MyERC1155Contract", "walletId": "", "templateParameters": { "name": "MyERC1155Contract", "defaultAdmin": "", "primarySaleRecipient": "", "royaltyRecipient": "", "royaltyPercent": "0", }, "feeLevel": "MEDIUM" }) request.template_parameters["royaltyPercent"] = 0 response = api_instance.deploy_contract_template("aea21da6-0aa2-4971-9a1a-5098842b1248", request) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/templates/aea21da6-0aa2-4971-9a1a-5098842b1248/deploy' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --header 'authorization: Bearer ' \ --data '{ "idempotencyKey": "", "blockchain": "ARC-TESTNET", "name": "MyERC1155Contract", "walletId": "", "templateParameters": { "name": "MyERC1155Contract", "defaultAdmin": "", "primarySaleRecipient": "", "royaltyRecipient": "", "royaltyPercent": 0 }, "feeLevel": "MEDIUM", "entitySecretCiphertext": "" }' ``` **Response:** ```json theme={null} { "data": { "contractIds": ["b7c35372-ce69-4ccd-bfaa-504c14634f0d"], "transactionId": "601a0815-f749-41d8-b193-22cadd2a8977" } } ``` A successful response indicates deployment has been **initiated**, not completed. Use the `transactionId` to check status. #### 2.1. Check deployment status Verify deployment with [`GET /transactions/{id}`](/api-reference/wallets/developer-controlled-wallets/get-transaction): ```javascript Node.js theme={null} const response = await circleDeveloperSdk.getTransaction({ id: "601a0815-f749-41d8-b193-22cadd2a8977", }); ``` ```python Python theme={null} api_instance = developer_controlled_wallets.TransactionsApi(client) response = api_instance.get_transaction(id="601a0815-f749-41d8-b193-22cadd2a8977") ``` ```shell cURL theme={null} curl --request GET \ --url 'https://api.circle.com/v1/w3s/transactions/601a0815-f749-41d8-b193-22cadd2a8977' \ --header 'accept: application/json' \ --header 'authorization: Bearer ' ``` **Response:** ```json theme={null} { "data": { "transaction": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "blockchain": "ARC-TESTNET", "state": "COMPLETE" } } } ``` ### Step 3: Mint a token Use the `mintTo` function to mint tokens. The wallet must have `MINTER_ROLE`. ```javascript Node.js theme={null} const response = await circleDeveloperSdk.createContractExecutionTransaction({ walletId: "", abiFunctionSignature: "mintTo(address,uint256,string,uint256)", abiParameters: [ "", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1", ], contractAddress: "", fee: { type: "level", config: { feeLevel: "MEDIUM", }, }, }); ``` ```python Python theme={null} api_instance = developer_controlled_wallets.TransactionsApi(client) request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({ "walletId": "", "abiFunctionSignature": "mintTo(address,uint256,string,uint256)", "abiParameters": [ "", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1" ], "contractAddress": "", "feeLevel": "MEDIUM", }) response = api_instance.create_developer_transaction_contract_execution(request) ``` ```shell cURL theme={null} curl --request POST \ --url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \ --header 'authorization: Bearer ' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "abiFunctionSignature": "mintTo(address,uint256,string,uint256)", "abiParameters": [ "", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei", "1" ], "idempotencyKey": "", "contractAddress": "", "feeLevel": "MEDIUM", "walletId": "", "entitySecretCiphertext": "" }' ``` **Response:** ```json theme={null} { "data": { "id": "601a0815-f749-41d8-b193-22cadd2a8977", "state": "INITIATED" } } ``` Check the transaction status using [`GET /transactions/{id}`](/api-reference/wallets/developer-controlled-wallets/get-transaction) as shown above. *** ## Summary After completing this quickstart, you've successfully: * Deployed an ERC-1155 Multi-Token contract on Arc Testnet * Minted your first token using either the Console or API ## Next steps * Explore the [Templates Glossary](/contracts/scp-templates-overview) for other contract templates * Learn about [Gas Station](/wallets/gas-station) for sponsoring transactions * View your contract on the [Arc Testnet Explorer](https://testnet.arcscan.app/) # Multi-Token Template Source: https://developers.circle.com/contracts/erc-1155-multi-token The Multi-Token template is an audited, ready-to-deploy smart contract for the ERC-1155 multi-token standard. ERC-1155 is a versatile token standard that allows for creating and managing multiple types of tokens within a single smart contract. Unlike other token standards, like ERC-20 and ERC-721, ERC-1155 supports fungible and non-fungible tokens, providing flexibility for various use cases. The ERC-1155 standard enables the creation of tokens representing different types of assets, such as digital collectibles, in-game items, unique artwork, and more, all within the same contract. This reduces the need to deploy separate contracts for different token types, improving efficiency and reducing costs. Some use cases of the standard include: * **Gaming assets:** With ERC-1155, developers can create game assets that can be fungible or non-fungible. For example, fungible ERC-1155 tokens can represent in-game currencies, while non-fungible ERC-1155 tokens can represent unique weapons, characters, or virtual land. * **Digital collectibles:** Similar to ERC-721, ERC-1155 can be used to create and trade digital collectibles. However, ERC-1155 offers additional flexibility, allowing for the creation of fungible and non-fungible tokens under the same contract. This enables the creation of collections with varying levels of scarcity and uniqueness. * **Tokenized real-world assets:** ERC-1155 tokens can also represent ownership of real-world assets such as real estate or shares in a company. By combining fungible and non-fungible tokens, ERC-1155 offers a more efficient solution for fractional ownership of assets. * **Batch operations:** One of the significant advantages of ERC-1155 is the ability to perform batch operations. Developers can transfer multiple tokens in a single transaction, making it more cost-efficient and reducing gas fees. In this comprehensive guide, you explore the Multi-Token template, which provides all the necessary information to deploy and understand the contract's common functions. ## Deployment parameters The Multi-Token template creates a smart contract representing and controlling any number of token types. These tokens can of the ERC-20, ERC-721 or any other standard. To create a contract using this template, provide the following parameter values when deploying a smart contract template using the [`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template) API. **Template ID:** aea21da6-0aa2-4971-9a1a-5098842b1248 ### Template deployment parameters | Parameter | Type | Required | Description | | ---------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | String | X | Name of the contract - stored on-chain. | | `symbol` | String | | Symbol of the token - stored onchain. The symbol is usually 3 or 4 characters in length. | | `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract. | | `primarySaleRecipient` | String | X | The recipient address for first-time sales. | | `platformFeeRecipient` | String | | The recipient address for all sale fees.  | | `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to platformFeeRecipient.  | | `royaltyRecipient` | String | X | The recipient address for all royalties (secondary sales). This allows the contract creator to benefit from further sales of the contract token. | | `royaltyPercent` | Float | X | The percentage of secondary sales that go to the royalty recipient. For example, set it as 0.05 if you want royalties to be 5% of secondary sales. | | `contractUri` | String | | The URL for the marketplace metadata of your contract.  | | `trustedForwarders` | String\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. | Here is an example of the `templateParameters`JSON object within the request body to [deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template) for the ERC-1155 Multi-Token template. In this example, the `defaultAdmin`, `primarySaleRecipient`, and `royaltyRecipient` parameters are the same address but can be set distinctly based on your use case. ```json JSON theme={null} ... "templateParameters": {   "name": "My Multi-Token Contract",   "defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",   "primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",   "royaltyRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",   "royaltyPercent": 0.05 } ``` ## Common functions This section lists the most commonly used functions on the Multi-Token template, along with their respective parameters and potential failure scenarios. These functions include: * [mintTo \[write\]](#mintto-write) * [safeTransferFrom \[write\]](#safetransferfrom-write) * [setApprovalForAll \[write\]](#setapprovalforall-write) * [setTokenURI \[write\]](#settokenuri-write) * [burn \[write\]](#burn-write) * [safeBatchTransferFrom \[write\]](#safebatchtransferfrom-write) * [balanceOfBatch \[read\]](#balanceofbatch-read) * [balanceOf \[read\]](#balanceof-read) * [nextTokenIdToMint \[read\]](#nexttokenidtomint-read) * [uri \[read\]](#uri-read) At this time, not all failure scenarios or error messages received from the blockchain are passed through Circle's APIs. Instead, you will receive a generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error. If available, the `errorDetails` field will have more information on the cause of failure. ## mintTo \[write] The `mintTo` function allows you to create new NFTs or increase the supply of existing NFTs. It is a flexible function that can cater to both scenarios. ### Parameters | Parameter | Type | Description | | :--------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_to` | address | The address to which the newly minted NFT will be assigned. | | `_tokenId` | unit256 | The unique identifier for the NFT. If the value is set to `type(uint256).max`, the function will assign the next available token ID. Otherwise, it will assign the provided `_tokenId` value. | | `_uri` | calldata | The Uniform Resource Identifier (URI) for the NFT's metadata. It specifies the location from where the metadata can be retrieved. | | `_amount` | unit256 | The amount of the newly minted NFTs to be assigned. | ### Failure scenarios * **Insufficient Role:** The `mintTo` function is defined with the `onlyRole(MINTER\_ROLE)` modifier, meaning only addresses with the `MINTER\_ROLE` can call this function. The function will revert and fail if the caller does not have the necessary role. * **Token ID Overflow:** If the `_tokenId` parameter is set to `type(uint256).max` (the maximum value for a uint256), the function will attempt to create a new token and assign the next available token ID. However, an overflow can occur if the `nextTokenIdToMint` variable has reached its maximum value. This overflow condition will cause the function to fail. * **Invalid Token ID:** If the `_tokenId` parameter is not set to `type(uint256).max`, the function will attempt to mint an NFT with the specified token ID. However, if the provided `_tokenId` value is greater than or equal to the value of `nextTokenIdToMint`, the function will revert and fail with the following error message.\ *"invalid id"* * **Existing Token ID with Non-Empty URI:** When `_tokenId` is provided and already exists, the function checks whether the associated metadata URI for that token ID is empty. If the URI is not empty, the token has already been minted and has an associated URI. In this case, the function will fail and revert, preventing the same token ID from being minted multiple times. * **Minting to Zero Address:** The function checks whether the `_to` address is the zero address `address(0)`. Minting tokens to the zero address is prohibited, as it represents an invalid or non-existent address. If `_to` is the zero address, the function will fail and revert with the following error message.\ *"ERC1155: mint to the zero address"* * **Rejection by ERC1155Receiver Contract:** If the recipient address `_to` is a contract, the function will attempt to call the `onERC1155Received` function of that contract to check if the contract supports receiving the NFT. If the contract's `onERC1155Received` function rejects the transfer by returning a value other than `IERC1155ReceiverUpgradeable.onERC1155Received.selector`, the function will revert and fail with the following error message.\ \_ "ERC1155: ERC1155Receiver rejected tokens"\_ ### Notes * **Creating New NFTs:** When you pass `type(uint256).max` via the `_tokenId` parameter, the function will create a new NFT with `_tokenId` equal to `nextTokenIdToMint` and assign it to the specified `_to` address. The `_uri` parameter allows you to provide the metadata URI for the newly created NFT. The `amount` parameter allows you to specify how many instances of this NFT with the given ID should be minted. * **Increasing Supply of Existing NFTs:** If you pass an existing token ID via the `tokenId` parameter, the function will increase the supply of that specific NFT. Instead of creating a new token ID, the function will mint additional instances of the existing NFT, adding to the current supply. Again, the `_amount` parameter determines how many additional instances of the NFT should be minted. ```solidity Solidity theme={null} // Lets an account with MINTER_ROLE mint an NFT. function mintTo( address _to, uint256 _tokenId, string calldata _uri, uint256 _amount ) external onlyRole(MINTER_ROLE) { uint256 tokenIdToMint; if (_tokenId == type(uint256).max) { tokenIdToMint = nextTokenIdToMint; nextTokenIdToMint += 1; } else { require(_tokenId < nextTokenIdToMint, "invalid id"); tokenIdToMint = _tokenId; } // `_mintTo` is re-used. `mintTo` just adds a minter role check. _mintTo(_to, _uri, tokenIdToMint, _amount); } ``` ## safeTransferFrom \[write] The `safeTransferFrom` function allows for transferring a specified amount of a particular token ID from one address `from` to another address `to`. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `from` | address | The address of the current token owner, from whom the tokens will be transferred. | | `to` | address | The address of the recipient who will receive the transferred tokens. | | `id` | uint256 | The unique identifier for transferring the token. | | `amount` | uint256 | The amount of tokens being transferred. This represents the number of tokens to be transferred. | | `data` | bytes | Optional additional data to pass to the receiver contract if it is a contract. This can include custom arguments or instructions for the receiving contract. | ### Failure scenarios * **Transfer to Zero Address:** The function verifies if the to address is the zero address `address(0)`. Transfers to the zero address are not permitted, as it represents an invalid or non-existent address. If the `to` address is the zero address, the function fails and reverts with the following error message.\ *"ERC1155: transfer to the zero address"* * **Insufficient Balance:** The function checks if the from address has a sufficient balance of the specified token ID (id) to perform the transfer. If the balance exceeds the specified amount, the function fails and reverts to the following error message.\ *"ERC1155: insufficient balance for transfer"* * **Caller Not Authorized:** The function verifies if the caller of the `_msgSender()` function is either the owner of the tokens (`from`) or has been approved as an operator for `from`. If the caller is neither the token owner nor an approved operator, the function fails and reverts with the following error message.\ *"ERC1155: caller is not token owner or approved"* * **Before/After Token Transfer Hooks:** The function calls the `_beforeTokenTransfer` and `_afterTokenTransfer` hooks to update any necessary state or perform additional checks. These hooks may contain custom business logic that can cause the transfer to fail if certain conditions are not met. * **ERC1155Receiver Contract Rejection:** If the `to` address is a contract, the function attempts to call the `onERC1155Received` function of that contract to check if the contract supports receiving the tokens. If the contract's `onERC1155Received` function rejects the transfer by returning a value other than `IERC1155ReceiverUpgradeable.onERC1155Received.selector`, the function fails and reverts with the following error message.\ *"ERC1155: ERC1155Receiver rejected tokens"* ```solidity Solidity theme={null} // See IERC1155-safeTransferFrom. function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes memory data ) public virtual override { require( from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner or approved" ); _safeTransferFrom(from, to, id, amount, data); } ``` ## setApprovalForAll \[write] The `setApprovalForAll` function is used to set the approval status for an operator to manage all tokens of the caller (owner) on their behalf. ### Parameters | Parameter | Type | Description | | :--------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------- | | `operator` | address | The operator's address for whom the approval status is set. The operator will be able to manage all tokens owned by the caller. | | `approved` | bool | The boolean value indicates whether the operator is approved (true) or disapproved (false) to manage all tokens on behalf of the caller. | ### Failure scenarios * The function requires that the caller (owner) cannot set the approval status for themselves. If the operator address provided is the same as the owner address, the function will fail with the given following error message.\ *"ERC1155: setting approval status for self"* ### Notes * Once an operator is approved using the `setApprovalForAll` function, they can act on behalf of the token owner. This includes performing actions such as transferring tokens. ```solidity Solidity theme={null} // See {IERC1155-setApprovalForAll}. function setApprovalForAll(address operator, bool approved) public virtual override { _setApprovalForAll(_msgSender(), operator, approved); } // Approve `operator` to operate on all of `owner` tokens // Emits an {ApprovalForAll} event. function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { require(owner != operator, "ERC1155: setting approval status for self"); _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } ``` ## setTokenURI \[write] The `setTokenURI` function is used to set the metadata URI for a given NFT. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :--------------------------------------------------------------------------- | | `tokenId` | unit256 | The unique identifier of the NFT for which the metadata URI needs to be set. | | `uri` | string | The URI string represents the metadata's location associated with the NFT. | ### Failure scenarios * The function requires the metadata URI to have a length greater than zero. This error occurs when the input parameter `_uri` is an empty string.\ \_"NFTMetadata: empty metadata" \_ * This error occurs if the `_canSetMetadata()` function returns false. It indicates that the caller has no authority or permission to set the metadata for the given NFT.\ *"NFTMetadata: not authorized to set metadata"* * This error occurs when `uriFrozen` is true, indicating that the metadata is frozen and cannot be updated.\ *"NFTMetadata: metadata is frozen"* ```solidity Solidity theme={null} // Sets the metadata URI for a given NFT. function setTokenURI(uint256 _tokenId, string memory _uri) public virtual { require(_canSetMetadata(), "NFTMetadata: not authorized to set metadata."); require(!uriFrozen, "NFTMetadata: metadata is frozen."); _setTokenURI(_tokenId, _uri); } // Sets the metadata URI for a given NFT. function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual { require(bytes(_uri).length > 0, "NFTMetadata: empty metadata."); _tokenURI[_tokenId] = _uri; emit MetadataUpdate(_tokenId); } ``` ## burn \[write] This function allows a token owner to burn a specified amount (value) of tokens they own. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------------- | | `account` | address | The address of the token owner who wants to burn their tokens. | | `id` | unit256 | The unique identifier of the token to be burned. | | `value` | unit256 | The amount of tokens to be burned. | ### Failure scenarios * This error occurs when the caller of the burn function is neither the owner of the tokens nor approved to burn them. The caller must either be the account that owns the tokens or have approval from the owner to burn the tokens.\ \_"ERC1155: caller is not owner nor approved" \_ * The function checks that the burning amount does not exceed the available balance. This error occurs if the amount of tokens specified to be burned (`amount`) is greater than the balance of tokens (`fromBalance`) owned by the specified account.\ *"ERC1155: burn amount exceeds balance"* * This error occurs if the from address (the address from which the tokens are being burned) is the zero address (0x000...). This address is generally reserved as an invalid or non-existent address and cannot be used for token burning.\ *"ERC1155: burn from the zero address"* ```solidity Solidity theme={null} // Lets a token owner burn the tokens they own (i.e. destroy for good) function burn(address account, uint256 id, uint256 value) public virtual { require( account == _msgSender() || isApprovedForAll(account, _msgSender()), "ERC1155: caller is not owner nor approved." ); _burn(account, id, value); } ``` ## safeBatchTransferFrom \[write] This function enables the safe transfer of multiple ERC1155 tokens from one address (`from`) to another address (`to`) in a batch. ### Parameters | Parameter | Type | Description | | :-------- | :--------- | :------------------------------------------------------------------------------------- | | `from` | address | The address from which the tokens are transferred. | | `to` | address | The address to which the tokens are transferred. | | `ids` | uint256\[] | An array of unique identifiers of the tokens to be transferred. | | `amounts` | uint256\[] | An array specifying the corresponding amounts of tokens to be transferred for each ID. | | `data` | bytes | Additional data to pass along with the transfer. Optional parameter. | ### Failure scenarios * This error occurs if the caller of the function is neither the token owner nor approved to perform the transfer. The caller must either be the from address or have approval from the from address to transfer the tokens.\ *"ERC1155: caller is not token owner or approved"* * This error occurs if the lengths of the `ids` and amounts arrays do not match. Each ID should have a corresponding amount to be transferred. The arrays should have the same length.\ *"ERC1155: ids and amounts length mismatch"* * This error occurs if the `to` address is the zero address (0x000...). Transferring tokens to the zero address is not allowed as it is generally used to represent an invalid or non-existent address.\ *"ERC1155: transfer to the zero address"* * This error occurs if the from address does not have a sufficient balance of tokens to transfer. The function checks that the from address has enough tokens of each ID to fulfill the transfer.\ *"ERC1155: insufficient balance for transfer"* * This error occurs if the to address is a contract and the contract does not implement the `onERC1155BatchReceived` function from the `IERC1155ReceiverUpgradeable` interface or if the function returns a value other than `onERC1155BatchReceived.selector`. This check ensures that the receiving contract can handle the transferred tokens properly.\ *"ERC1155: ERC1155Receiver rejected tokens"* ```solidity Solidity theme={null} // IERC1155-safeBatchTransferFrom function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public virtual override { require( from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner or approved" ); _safeBatchTransferFrom(from, to, ids, amounts, data); } ``` ## balanceOfBatch \[read] The `balanceOfBatch` function retrieves the balances of multiple accounts for multiple token IDs in a single function call. ### Parameters | Parameter | Type | Description | | :--------- | :--------- | :------------------------------------------------------------------------- | | `accounts` | address\[] | An array of addresses representing the accounts to query the balances for. | | `ids` | unit256\[] | An array of unique identifiers of the tokens to query the balances for. | ### Failure scenarios * **Mismatched Array Lengths:** The function requires that the length of the accounts array is equal to the length of the `ids` array. If this condition is not met, it will throw a required exception with the following error message.\ *"ERC1155: accounts and ids length mismatch"* ```solidity Solidity theme={null} // IERC1155-balanceOfBatch // Requirements: // `accounts` and `ids` must have the same length. function balanceOfBatch( address[] memory accounts, uint256[] memory ids ) public view virtual override returns (uint256[] memory) { require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); uint256[] memory batchBalances = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; ++i) { batchBalances[i] = balanceOf(accounts[i], ids[i]); } return batchBalances; } ``` ## balanceOf \[read] The `balanceOf` function retrieves the balance of a specific account for a particular token ID. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------------------ | | `account` | address | The EVM address for which the balance is being queried. | | `id` | unit256 | The unique token identifier for which the balance is being queried. | ### Failure scenarios * **Zero Address:** The function requires that the account parameter is not set to the zero address `address(0)`. If this condition is not met, it will throw a required exception with the following error message.\ *"ERC1155: address zero is not a valid owner".* ```solidity Solidity theme={null} // See IERC1155-balanceOf // Requirements: // account cannot be the zero address. function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { require(account != address(0), "ERC1155: address zero is not a valid owner"); return _balances[id][account]; } ``` ## uri \[read] The URI function retrieves the associated URI with a specific token ID. This URI provides a way to access metadata and additional information about the token. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :---------------------------------------------------------- | | `tokenId` | unit256 | This is the unique token identifier for retrieving the URI. | ```solidity Solidity theme={null} // Returns the URI for a tokenId function uri(uint256 _tokenId) public view override returns (string memory) { return _tokenURI[_tokenId]; } ``` ## Public Variables Public variables are accessible from within the contract and can be accessed from external contracts. Solidity automatically generates a getter function for public state variables. ## nextTokenIdToMint \[read] The `nextTokenIdToMint` variable is a public constant on the smart contract. An unsigned integer (uint256) represents the next token ID minted or created when `type(uint256).max` is passed to the `mintTo` function. ```solidity Solidity theme={null} // The next token ID of the NFT to mint. uint256 public nextTokenIdToMint; ``` # Token Template Source: https://developers.circle.com/contracts/erc-20-token The Token template is an audited, ready-to-deploy smart contract for an ERC-20 token. The ERC-20 standard is the most popular standard for fungible tokens. The token's fungibility allows it to be used for a variety of use cases, including: * **Stablecoins:** ERC-20 tokens serve as the foundation for stablecoins like USDC, backed by US dollars. To learn more about the USDC contract, see [0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) on Etherscan. * **Loyalty points:** ERC-20 tokens can represent on-chain loyalty points to incentivize and reward users for their activities within a platform or ecosystem. * **Governance:** ERC-20 tokens can represent governance rights, allowing holders to participate in protocol decisions. Notable examples include tokens utilized by platforms like Uniswap. * **Ownership:** ERC-20 tokens can also represent fractional ownership in real-world assets, such as houses, ounces of gold, or company shares. In this comprehensive guide, you explore the Token template, which provides all the necessary information to deploy and understand the contract's common functions. ## Deployment parameters The Token template creates a customized, fully compliant ERC-20 smart contract. To create a contract using this template, provide the following parameter values when deploying a smart contract template using the [`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template) API. **Template ID:** a1b74add-23e0-4712-88d1-6b3009e85a86 ### Template deployment parameters | Parameter | Type | Required | Description | | ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | String | X | Name of the contract - stored as a property of the contract on-chain.  | | `symbol` | String | | Symbol of the token - stored on-chain. The symbol is usually 3 or 4 characters in length. | | `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract.
**Important:** You will lose administrative access to the contract if this is not set to an address you control. | | `primarySaleRecipient` | String | X | The recipient address for first-time sales.   | | `platformFeeRecipient` | String | | The recipient address for all sales fees. If you deploy a template on someone else's behalf, you can set this to your own address. | | `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to the  `platformFeeRecipient`.  | | `contractUri` | String | | The URL for the marketplace metadata of your contract. This is used on marketplaces like OpenSea. See [Contract-level Metadata](https://docs.opensea.io/docs/contract-level-metadata) for more information.  | | `trustedForwarders` | Strings\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. See [ethereum.org](https://eips.ethereum.org/EIPS/eip-2771) for more information.  | Here is an example of the `templateParameters` JSON object within the request body to [deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template) for the ERC-20 Token template. In this example, the `defaultAdmin` and `primarySaleRecipient` parameters are the same address but can be set distinctly based on your use case. ```json JSON theme={null} "templateParameters": { "name": "My Token Contract", "defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6", "primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6" } ``` ## Common functions This section lists the most commonly used functions on the Token template, their respective parameters and potential failure scenarios. These functions include: * [approve \[write\]](#approve-write) * [transfer \[write\]](#transfer-write) * [mintTo \[write\]](#mintto-write) * [burn \[write\]](#burn-write) * [grantRole \[write\]](#grantrole-write) * [revokeRole \[write\]](#revokerole-write) * [balanceOf \[read\]](#balanceof-write) * [allowance \[read\]](#allowance-write) At this time, not all failure scenarios or error messages received from the blockchain are passed through Circle's APIs. Instead, you will receive a generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error. If available, the `errorDetails` field will have more information on the cause of failure. ## approve \[write] The `approve` function lets token owners specify a limit on the number of tokens another account address can spend on their behalf. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | | `spender` | address | The owner approves the account address to spend tokens. It could be another smart contract address or an externally owned account address. | | `amount` | unit256 | The number of tokens the owner approves for spending by the specified spender. The amount should be set in the smallest denomination of the ERC-20 token. | ### Failure scenarios * The approve function fails if the `spender` is the zero address.\ *"ERC20: approve to the zero address"* ### Notes * If `amount` is the maximum uint256 value the allowance is not updated when the `transferFrom` function is called. This is semantically equivalent to an infinite approval. * There is no balance check on the `approve` function so token owners may approve allowances greater than their current balance. ```solidity Solidity theme={null} function approve(address spender, uint256 amount) public virtual override returns (bool) { address owner = _msgSender(); _approve(owner, spender, amount); return true; } ``` ## transfer \[write] Allows the token owner to transfer a specified number of tokens to another account address. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------------------------------------------------------------------ | | `to` | address | The address to which the token will be transferred. This can be another user's address or a smart contract address. | | `amount` | unit256 | The number of tokens to transfer. | ### Failure scenarios * The `to` address parameter is checked to ensure it is not the zero address `address(0)`.\ *"ERC20: transfer to the zero address"* * The `beforeTokenTransfer` hook is called, which checks `TOKEN_TRANSFER` permissions.\ "restricted to TRANSFER\_ROLE holders” * The token owner doesn't have a sufficient balance to transfer the specified token amount.\ *"ERC20: transfer amount exceeds balance"* ```solidity Solidity theme={null} function transfer(address to, uint256 amount) public virtual override returns (bool) { address owner = _msgSender(); _transfer(owner, to, amount); return true; } ``` ## mintTo \[write] A designated minter can create a specified number of tokens and assign them to a specified account address. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :----------------------------------------------------------------------- | | `to` | address | The address to which the newly minted tokens will be assigned. | | `amount` | unit256 | The number of tokens to be minted and assigned to the specified address. | ### Failure scenarios * The function checks if the caller has the `MINTER_ROLE`. The function will fail if the caller does not have the minter role. Expect to see the following error:\ *"not minter."* * The function ensures that the account address is not the zero address `address(0)`, as this is an invalid address to mint tokens to.\ *"ERC20: mint to the zero address"* * The `beforeTokenTransfer` hook is called, which checks `TOKEN_TRANSFER` permissions.\ *"restricted to* TRANSFER*ROLE \_holders”* ```solidity Solidity theme={null} function mintTo(address to, uint256 amount) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), "not minter."); _mintTo(to, amount); } ``` ## burn \[write] Allows a token holder to burn (destroy) a specified number of the token total supply. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :--------------------------------- | | `amount` | unit256 | The number of tokens to be burned. | ### Failure scenarios * The `_beforeTokenTransfer`  hook is called, which checks `TOKEN_TRANSFER` permissions.\ *"restricted to TRANSFER\_ROLE holders”* * The token owner has no sufficient balance to burn the specified token amount.\ *"ERC20: burn amount exceeds balance"* ```solidity Solidity theme={null} function burn(uint256 amount) public virtual { _burn(_msgSender(), amount); } ``` ### Permissions and roles Roles are referred to by their `bytes32` identifier. For example: ```solidity Solidity theme={null} bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); ``` Roles can also represent a set of permissions. To restrict access to a function call, use `hasRole`: ```solidity Solidity theme={null} function foo() public { require(hasRole(MY_ROLE, msg.sender)); ... } ``` Roles can be granted and revoked dynamically via the `grantRole` and `revokeRole` functions. Each role has an associated admin role, and only accounts that have a role's admin role can call `grantRole` and `revokeRole`. By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`. Only accounts with the `DEFAULT_ADMIN_ROLE` can grant or revoke other roles. You can use the `_setRoleAdmin` function to create more complex role relationships. ## grantRole \[write] Grants a specified role to an account. Only an account address that has the admin role assigned can call this function. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------ | | `role` | bytes32 | The bytes32 identifier of the role to be granted. | | `account` | address | The address to which the role will be granted. | ### Failure scenarios * The `onlyRole(getRoleAdmin(role))` modifier ensures that the function caller has the admin role for the specified role. Only addresses with the admin role can grant roles to other accounts.\ *"AccessControl: account ", StringsUpgradeable.toHexString(account), " is missing role ", StringsUpgradeable.toHexString(uint256(role), 32)* * The function checks if the account already has the specified role using the `hasRole` function. If the account does not have the role, the function continues. There is no error message. ```solidity Solidity theme={null} function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { _grantRole(role, account); } ``` ## revokeRole \[write] Revoke the specified role from an account address. Only account addresses with the admin role assigned can call this function. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :----------------------------------------------- | | `role` | bytes32 | The role to be revoked. | | `account` | address | The address from which the role will be revoked. | ### Failure scenarios * The `onlyRole(getRoleAdmin(role))` modifier ensures that the function caller has the admin role for the specified role. Only addresses with the admin role can revoke roles from other accounts.\ *"AccessControl: account ", StringsUpgradeable.toHexString(account), " is missing role ", StringsUpgradeable.toHexString(uint256(role), 32)* ```solidity Solidity theme={null} function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { _revokeRole(role, account); } ``` ## balanceOf \[read] Retrieves the balance of tokens owned by a specific account address. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :-------------------------------------------------------- | | `account` | address | The address for which the token balance is being fetched. | ```solidity Solidity theme={null} function balanceOf(address account) public view virtual override returns (uint256) { return _balances[account]; } ``` ## allowance \[read] Returns the maximum amount the spender is approved to withdraw from the owner's account. This function retrieves the allowance granted by the owner account address to the spender account address. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :---------------------------------------------------- | | `owner` | address | The address that granted the allowance. | | `spender` | address | The address for which the allowance is being fetched. | ```solidity Solidity theme={null} function allowance(address owner, address spender) public view virtual override returns (uint256) { return _allowances[owner][spender]; } ``` # NFT Template Source: https://developers.circle.com/contracts/erc-721-nft The NFT template is an audited, ready-to-deploy smart contract for creating and managing NFTs. It implements the ERC-721 standard, which is widely used for representing non-fungible tokens (NFTs) on a blockchain. Unlike ERC-20 tokens, which represent fungible and interchangeable assets, ERC-721 NFTs are unique and non-interchangeable, making them suitable for digital collectibles, gaming assets, and many other use cases. The ERC-721 NFT standard has gained significant popularity and has been implemented by numerous projects and platforms. Some key use cases for ERC-721 NFTs include: * **Digital collectibles:** ERC-721 NFTs are extensively used for creating and trading unique digital collectibles. These collectibles can represent various items such as artwork, trading cards, virtual pets, in-game assets, and more. * **Tokenized assets:** ERC-721 NFTs can represent ownership in real-world assets such as real estate, artwork, jewelry, and other physical assets. This enables fractional ownership, providing liquidity and opening up investment opportunities. * **Gaming assets:** ERC-721 NFTs are a perfect fit for representing in-game assets, enabling players to own, trade, and transfer virtual items securely and transparently. This functionality has facilitated the emergence of blockchain-based gaming ecosystems. In this comprehensive guide, you explore the NFT template, which provides all the necessary information to deploy and understand the contract's common functions. ## Deployment parameters The NFT template creates a customized, fully compliant ERC-721 smart contract. To create a contract using this template, provide the following parameter values when deploying a smart contract template using the [`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template) API. **Template ID:** 76b83278-50e2-4006-8b63-5b1a2a814533 ### Template deployment parameters | Parameter | Type | Required | Description | | :--------------------: | --------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | `name` | String | X | Name of the contract - stored as a property of the contract on-chain.  | | `symbol` | String | | Symbol of the token - stored onchain. The symbol is usually 3 or 4 characters in length. | | `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract. You will lose administrative access to the contract if this is not set to an address you control. | | `primarySaleRecipient` | String | X | The recipient address for first-time sales.  | | `platformFeeRecipient` | String | | The recipient address for all sale fees. You can set this to your address if you are deploying a template on someone else's behalf. | | `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to *platformFeeRecipient*.  | | `royaltyRecipient` | String | X | The recipient address for all royalties (secondary sales). This allows the contract creator to benefit from further sales of the contract token. | | `royaltyPercent` | Float | X | The percentage of secondary sales that go to the royalty recipient. For example, set it as 0.05 if you want royalties to be 5% of secondary sales value. | | `contractUri` | String | | The URL for the marketplace metadata of your contract. This is used on marketplaces like OpenSea. See [Contract-level Metadata](https://docs.opensea.io/docs/contract-level-metadata) for more information.  | | `trustedForwarders` | String\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. See [ethereum.org](https://eips.ethereum.org/EIPS/eip-2771) for more information.  | Here is an example of the `templateParameters` JSON object within the request body to [deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template) for the ERC-721 NFT template. In this example, the `defaultAdmin`, `primarySaleRecipient`, and `royaltyRecipient` parameters are the same address but can be set distinctly based on your use case. ```json JSON theme={null} ... "templateParameters": { "name": "My NFT Contract", "defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6", "primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6", "royaltyRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6", "royaltyPercent": 0.05 } ``` ## Common functions This section lists the most commonly used functions on NFT template, their respective parameters and potential failure scenarios. These functions include: * [approve \[write\]](#approve-write) * [mintTo \[write\]](#mintto-write) * [safeTransferFrom \[write\]](#safetransferfrom-write) * [setTokenURI \[write\]](#settokenuri-write) * [ownerOf \[read\]](#ownerof-read) * [balanceOf \[read\]](#balanceof-address-owner-read) At this time, not all failure scenarios or error messages received from the blockchain are passed through Circle's APIs. Instead, you will receive a generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error. If available, the `errorDetails` field will have more information on the cause of failure. ## approve \[write] The approve function allows the owner of an ERC721 NFT to approve another address to transfer the token on their behalf. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------- | | `to` | address | The address approved to transfer the token. | | `tokenId` | unit256 | The identifier of the token being approved for transfer. | **Failure Scenarios:** * If the *to* address matches the current owner of the token (owner), the function will fail. This check ensures that the approval is not granted to the same owner, preventing unnecessary approvals. *"ERC721: approval to current owner"* * The function requires that the caller `_msgSender` either be the token's owner or have been approved for all by the owner. If this condition is not met, the function will fail. This validation prevents unauthorized users from approving transfers on behalf of the token owner. *"ERC721: approve caller is not token owner or approved for all"* ```solidity Solidity theme={null} function approve(address to, uint256 tokenId) public virtual override { address owner = ERC721Upgradeable.ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); require( _msgSender() == owner || isApprovedForAll(owner, _msgSender()), "ERC721: approve caller is not token owner or approved for all" ); _approve(to, tokenId); } ``` ## mintTo \[write] The `mintTo` function is a function that mints a new NFT and assigns it to a specific address. This function can only be called by an address with the `MINTER_ROLE`. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------------- | | `to` | address | The address to which the minted NFT will be assigned. | | `uri` | string | The URI (Uniform Resource Identifier) of the newly minted NFT. | ### Returns | Parameter | | | | :-------------- | :------ | :--------------------------------------- | | `tokenIdToMint` | uint256 | The unique identifier of the minted NFT. | ### Failure scenarios * If the caller of the function does not have the `MINTER_ROLE` assigned, the function will fail and throw an exception. *"AccessControl: account ", StringsUpgradeable.toHexString(account), " is missing role ", StringsUpgradeable.toHexString(uint256(role), 32)* * The function checks if the length of the \_uri string is greater than 0, ensuring that the URI is not empty.\ *"empty uri."* * *The function checks that the to address is not the zero address.*\ *"ERC721: mint to the zero address"* * The function checks that the `tokenId` has not already been created. *"ERC721: token already minted"* ```solidity Solidity theme={null} function mintTo(address _to, string calldata _uri) external onlyRole(MINTER_ROLE) returns (uint256) { // `_mintTo` is re-used. `mintTo` just adds a minter role check. return _mintTo(_to, _uri); } function _mintTo(address _to, string calldata _uri) internal returns (uint256 tokenIdToMint) { tokenIdToMint = nextTokenIdToMint; nextTokenIdToMint += 1; require(bytes(_uri).length > 0, "empty uri."); _setTokenURI(tokenIdToMint, _uri); _safeMint(_to, tokenIdToMint); emit TokensMinted(_to, tokenIdToMint, _uri); } ``` ## safeTransferFrom \[write] This function allows the transfer of an ERC721 NFT from the `from` address to the `to` address. It requires that the caller is the token owner or has been approved to transfer the token. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :-------------------------------------------------------- | | `from` | address | The address that owns the token and wants to transfer it. | | `to` | address | The address that will receive ownership of the token. | | `tokenId` | unit256 | The unique identifier of the token being transferred. | ### Failure scenarios * The `isApprovedOrOwner` function is called to check if the caller is the token owner or an approved address. This check ensures that the transfer can only be performed by the token owner or an approved address.\ *"ERC721: caller is not token owner or approved"* * If the to address is a contract, the `checkOnERC721Received` function is called to check if the to address is a contract that implements the `onERC721Received` function correctly - according to the ERC721 standard. * It checks if the token being transferred `tokenId` parameter is owned by the `from` address parameter. *"ERC721: transfer from incorrect owner"* * It checks that the `to` address parameter is not the zero address. If it is the zero address, the function throws an exception with the message.\ *"ERC721: transfer to the zero address"* * If the transfer is restricted on the contract, it still allows burning and minting. It checks whether the `TRANSFER_ROLE` is assigned to either the `from` or `to` address. This ensures that token transfers comply with specific access control restrictions defined by the contract.\ *"restricted to TRANSFER\_ROLE holders”* * The function will check if the `to` address is a contract. If it is, the `_checkOnERC721Received` hook will check if the receiver address properly handles the received token. I\ *"ERC721: transfer to non ERC721Receiver implementer"* ```solidity Solidity theme={null} // safeTransferFrom function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { safeTransferFrom(from, to, tokenId, ""); } // safeTransferFrom - with data parameter function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); _safeTransfer(from, to, tokenId, data); } ``` ## setTokenURI \[write] This function is responsible for setting the metadata URI for a specific NFT token. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :------------------------------------------------------------------------------ | | `tokenId` | unit256 | The unique identifier of the NFT token for which the metadata URI is being set. | | `uri` | string | The new metadata URI that will be associated with the NFT token. | ### Failure scenarios * The function checks if the caller is authorized to set the metadata URI. It calls the `canSetMetadata` function, which checks the authorization based on certain conditions specified in the contract.\ *"NFTMetadata: not authorized to set metadata."* * The function verifies if the metadata URI is not frozen. It checks the `uriFrozen` boolean flag to determine if the metadata is in a frozen state. If the metadata is frozen, meaning it cannot be changed, the function will throw an exception.\ *"NFTMetadata: metadata is frozen."* * If the provided URI is empty, the function will throw an exception with the message\ *"NFTMetadata: empty metadata."* ```solidity Solidity theme={null} function setTokenURI(uint256 _tokenId, string memory _uri) public virtual { require(_canSetMetadata(), "NFTMetadata: not authorized to set metadata."); require(!uriFrozen, "NFTMetadata: metadata is frozen."); _setTokenURI(_tokenId, _uri); } function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual { require(bytes(_uri).length > 0, "NFTMetadata: empty metadata."); _tokenURI[_tokenId] = _uri; emit MetadataUpdate(_tokenId); } ``` ## ownerOf \[read] This function is used to retrieve the address of the owner of the ERC721 NFT with the specified `tokenId`. ### Parameters | Parameter | Type | Description | | :-------- | :------ | :--------------------------------------------------------------------------------- | | `tokenId` | unit256 | The unique identifier of the token for which the owner's address is being fetched. | ### Failure scenarios * The function checks if the owner's address is not the zero address. This check is performed to ensure that a valid owner address is returned. *"ERC721: invalid token ID"* ### Note * The function does not revert if the token doesn't exist. The zero address will be returned. ```solidity Solidity theme={null} function ownerOf(uint256 tokenId) public view virtual override returns (address) { address owner = _ownerOf(tokenId); require(owner != address(0), "ERC721: invalid token ID"); return owner; } ``` ## balanceOf \[read] This function retrieves the balance (number of tokens) owned by a specific owner address. ### Parameters | Parameter | | | | :-------- | :------ | :-------------------------------------------------------- | | `owner` | address | The address for which the token balance is being fetched. | ### Failure scenarios * The function checks if the `owner` address is not the zero address. The zero address represents an invalid or nonexistent address. *"ERC721: address zero is not a valid owner"* ```solidity Solidity theme={null} function balanceOf(address owner) public view virtual override returns (uint256) { require(owner != address(0), "ERC721: address zero is not a valid owner"); return _balances[owner]; } ``` # Contracts API Error Codes Source: https://developers.circle.com/contracts/error-codes Descriptions of error codes returned by the Contracts API The Wallets and Contracts APIs return an [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) when they encounter an error in an API request: * `4xx` errors are client errors, which are informative and actionable. They communicate a mistake to the user and suggest fixes. * `5xx` errors are unexpected server-side errors. The tables in the following sections describe some of the common error messages you might encounter using the Wallets and Contracts APIs. Where possible, a suggested fix is provided in the `Description` column. ## General error format HTTP status codes do not always provide sufficient information to determine the cause of the error. Since the status code is part of the header of the response, the body of response contains additional, Circle-specific, error information. For example, if a request contains an invalid parameter, the response includes the following: **Header** `HTTP/1.1 400 Bad RequestContent-Type: application/json` **Body** ```json JSON theme={null} { "code": 2, "message": "API parameter invalid" } ``` ## Extended error format In some cases, the response includes extended information about the cause of the error. For example, if a request doesn't include a value for a required parameter, the response includes a detailed error message: **Header** `HTTP/1.1 400 Bad RequestContent-Type: application/json` **Body** ```json JSON theme={null} { "code": 2, "message": "API parameter invalid", "errors": [ { "error": "required", "message": "fail to bind request to CreateWalletSetRequest: EOF", "location": "field1", "invalidValue": "null", "constraints": {} } ] } ``` ## General errors This section describes some common extended error messages you might encounter using Wallets and Contracts APIs. The following errors are general errors that can be returned for any request. | Error Code | HTTP code | Error Message | Description | | :--------- | :-------- | :--------------------- | :--------------------------------------------------------------- | | `-1` | `400` | `Something went wrong` | An unknown error occurred while processing the API request. | | `3` | `403` | `Forbidden` | The API key used does not have access to the requested endpoint. | ### Invalid requests A `400` HTTP status code indicates that the request is invalid or malformed. When the request is invalid, the Circle-specific error code is `2`. The following table describes the common error messages that can be returned for invalid requests. | Error Code | HTTP code | Error Message | Description | | :--------- | :-------- | :------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------- | | `2` | `400` | `Invalid Entity` | There is an error in the JSON format passed in the request. | | `2` | `400` | `Fail to bind request to parameter: invalid UUID format` | The parameter must be in UUID format. | | `2` | `400` | `Error: Field validation` | One of the fields in the request is invalid. | | `2` | `400` | `Error: Field validation for blockchain failed on the blockchain tag` | The blockchain does not exist. Try again with a [supported blockchain](/wallets/supported-blockchains). | | `2` | `400` | `Error: Field validation for gasLimit failed on the required_without tag` | If `feeLevel` is provided, `gasLimit` should be set to `NULL`. If `feeLevel` is `NULL`, `gasLimit` must be provided. | | `2` | `400` | `Error: Field validation for parameter failed on the min tag` | The parameter is not in the correct format. | | `2` | `400` | `Cannot unmarshal` | The JSON body of the request is not valid. | | `2` | `400` | `INVALID: parameter empty` | The parameter is required for this request. Try the request again with this parameter. | ## Contracts errors This section includes the errors that can be returned from the Circle Contracts API requests. ### Contract errors | Error code | HTTP code | Error message | Description | | ---------- | --------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `175001` | `404` | `Contract not found.` | The specified contract does not exist in the system. | | `175002` | `400` | `No ABI JSON for the target contract.` | Cannot execute a read function on a contract without the ABI JSON. | | `175003` | `400` | `Constructor parameters length must match constructor signature.` | The number of constructor parameters must match the constructor signature. | | `175004` | `409` | `Contract already exists.` | The contract already exists in the system. | | `175005` | `400` | `Address is not a contract address.` | The given address is not associated with a smart contract. | | `175006` | `400` | `Contract is archived.` | Attempted to interact with an archived contract. | | `175007` | `400` | `Invalid ABI JSON.` | The inputted ABI JSON is not correctly formatted. | | `175008` | `400` | `Multi-layered proxies are not supported.` | Importing a multi-layered proxy contract is not supported. | | `175009` | `400` | `Contract deployment pending.` | Contract deployment must be completed before function execution is available. | | `175010` | `400` | `ABI function not found.` | The ABI function was not found on the contract. | | `175011` | `400` | `Empty update on a contract.` | An empty update for a contract is not allowed | | `175012` | `400` | `Unable to query contract.` | Unable to query contract. Check your parameters and try again. | | `175013` | `400` | `ABI function is not supported.` | The ABI function of the contract is not supported. | ### Template errors | Error code | HTTP code | Error message | Description | | ---------- | --------- | -------------------------------------------------- | ------------------------------------------------------------------- | | `175201` | `404` | `Template not found.` | The specified template does not exist in the system. | | `175202` | `400` | `Deploying this template is temporarily disabled.` | Deploying this template is temporarily disabled. | | `175203` | `400` | `Invalid template deployment parameter.` | The request contains an invalid field in the template parameters. | | `175204` | `400` | `Missing required template deployment parameter.` | The request is missing a required field in the template parameters. | | `175205` | `400` | `Estimation is not supported.` | Estimating the deployment of this template is not supported. | ### Event subscription errors | Error code | HTTP code | Error message | Description | | ---------- | --------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | | `175301` | `404` | `Event subscription not found.` | The specified event subscription does not exist or is not accessible to the caller. | | `175302` | `409` | `Event subscription already exist.` | The specified event has already been created for this contract. | | `175303` | `400` | `The specified event signature does not exist.` | The specified event signature does not exist on this contract. | ### Common errors | Error code | HTTP code | Error message | Description | | ---------- | --------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `175401` | `400` | `Fail to parse id as UUID in url.` | The specified ID is invalid (must be in UUID format). Try again with a valid ID. | | `175402` | `400` | `The specified blockchain is either not supported or deprecated.` | The specified blockchain is either not supported or deprecated. | | `175403` | `409` | `Please use a new idempotency key.` | Use a new idempotency key and try again. | | `175404` | `400` | `TEST_API key cannot be used with blockchain mainnets, or LIVE_API key cannot be used with blockchain testnets.` | TEST\_API key cannot be used with blockchain mainnets, or LIVE\_API key cannot be used with blockchain testnets. | | `175405` | `401` | `TEST_API key or LIVE_API key is not found for the request.` | TEST\_API key or LIVE\_API key is not found for the request. | | `175406` | `400` | `This feature is temporarily disabled.` | This feature is temporarily disabled. | | `175407` | `400` | `The specified blockchain is unavailable.` | The specified blockchain is unavailable. Check the [Circle Status page](https://status.circle.com/) for more details. | | `175408` | `404` | `Cannot find corresponding pagination cursor in the system.` | Cannot find corresponding pagination cursor in the system. | | `175409` | `403` | `Entities with restrictions cannot perform this operation.` | Entities with restrictions cannot perform this operation. | | `175410` | `400` | `invalid address format` | The address format is invalid. | ### Transaction errors | Error code | HTTP code | Error message | Description | | ---------- | --------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `177001` | `400` | `transaction nonce is inconsistent with sender's latest nonce` | The transaction nonce is inconsistent with the sender's latest nonce. | | `177002` | `400` | `user op nonce can not be larger than 0 when smart contract wallet hasn't been deployed` | User op nonce can not be larger than 0 when the smart contract wallet hasn't been deployed. | | `177003` | `400` | `failed to execute this request on EVM due to insufficient token when estimating fee` | Failed to execute this request on EVM due to insufficient tokens when estimating the fee. | | `177004` | `400` | `the total cost of executing transaction is higher than the balance of the user's account when estimating fee` | When estimating the fee, the total cost of executing the transaction is higher than the balance of the user's account. | | `177005` | `400` | `the sender address is not token owner or approved when estimating token transfer` | The sender address is not token owner or approved when estimating token transfer. | | `177006` | `400` | `gas required exceeds allowance when estimating fee` | Gas required exceeds allowance when estimating fee. | | `177007` | `400` | `estimate fee execution reverted` | Estimate fee execution reverted. | | `177008` | `400` | `ABI function signature can't pack ABI parameter` | ABI function signature can't pack ABI parameter. | | `177009` | `400` | `fails to perform transaction estimation` | Fails to perform transaction estimation. | | `177010` | `400` | `maxFee * gasLimit exceed configurable max transaction fee (default is 1 native token)` | The `MaxFee` \* `GasLimit` exceeds the configurable max transaction fee (default is 1 native token). | | `177011` | `400` | `transaction needs feeLevel or gasLimit provided` | The transaction requires `feeLevel` or `gasLimit` to be provided in the request. | | `177012` | `400` | `sca transaction needs feeLevel provided` | The SCA transaction requires `feeLevel` to be provided in the request. | | `177013` | `400` | `EIP1559 chains need maxFee/priorityFee provided` | EIP1559 chains require `maxFee` and `priorityFee` to be provided in the request. | | `177014` | `400` | `priorityFee cannot be larger than maxFee in creating transaction request` | Creating transaction requests`priorityFee` cannot be larger than `maxFee`. | | `177015` | `400` | `missing bytecode for contract deployment` | Missing bytecode for contract deployment. | | `177016` | `400` | `cannot provide both WalletID and SourceAddress/Blockchain` | You cannot provide `WalletID` and `SourceAddress`/`Blockchain` in a request. | | `177017` | `400` | `Invalid amount in contract execution request` | The `amount` in the contract execution request is invalid. | | `177018` | `400` | `policy is not activated and cannot be used` | The Gas Station paymaster policy is not activated and cannot be used. | | `177019` | `400` | `exceeded max daily transaction of the policy` | The Gas Station paymaster policy maximum daily transaction limit has been reached. | | `177020` | `400` | `exceeded max spend USD per transaction of the policy` | The transaction cost exceeds the Gas Station paymaster policy maximum spend per transaction in USD. | | `177021` | `400` | `exceeded max spend USD daily of the policy` | The Gas Station paymaster policy for maximum spending daily in USD has been reached. | | `177022` | `400` | `exceeded max native token daily of the policy` | The Gas Station paymaster policy for maximum native tokens daily of the policy. | | `177023` | `400` | `sender is in policy blocklist` | The sender is on the Gas Station paymaster policy blocklist. | | `177024` | `400` | `wallet and request's blockchain mismatch.` | The wallet and blockchain in the request should be the same. | | `177301` | `400` | `wallet is Frozen` | Frozen wallets can not be updated or interacted with; they can only be queried. | | `177302` | `400` | `invalid sca wallet config` | The SCA wallet configuration is invalid. | | `177303` | `400` | `sca wallet first-time transaction is still in progress` | The SCA wallet needs to wait for the first-time transaction to finish deploying the wallet before processing more transactions. | | `177304` | `400` | `SCA account is not supported on the given blockchain` | The SCA account is not supported on the given blockchain. | | `177305` | `400` | `Entity is not eligible for SCA account creation. Please check paymaster policy setup` | The entity is not eligible for SCA account creation. Check the Gas Station paymaster policy setup. | | `177601` | `400` | `could be caused by either no such wallet or wallet is not accessible to the caller` | The target wallet cannot be found in the system. Either the specified wallet doesn't exist, or it's inaccessible to the caller. | | `177602` | `400` | `reusing an entity secret ciphertext is not allowed. Please re-encrypt the entity secret to generate new ciphertext` | Reusing an entity's secret ciphertext is not allowed. Re-encrypt the entity secret to generate a new ciphertext. | | `177603` | `400` | `entity is likely not properly set up during the onboarding process` | The corresponding entity cannot be found in the system. | | `177604` | `400` | `the provided entity secret is invalid` | The provided entity secret is invalid. | | `177605` | `400` | `the entity secret has not been set yet. Please provide encrypted ciphertext in the console` | The entity secret has not been set up on your account. Provide encrypted ciphertext in the console. | | `177606` | `400` | `current entity secret is invalid. Please rotate the entity secret first` | The provided entity secret is invalid. Rotate the entity secret first and send another API request. | | `177607` | `400` | `please use a new idempotency key` | Use a new idempotency key. | | `177901` | `400` | `smart contract query failed` | Error when querying contract. Check parameters and try again. | # Postman Collection Source: https://developers.circle.com/contracts/postman Use Circle's Postman collection to send API requests and explore the Smart Contract Platform APIs. Circle's Postman collection provides template requests to help you learn about the Smart Contract Platform APIs. These requests run on [Postman](https://www.postman.com/), an API platform for learning, building, and using APIs. The Postman Contracts workspace includes a collection that matches the organization of the [API reference](/api-reference/contracts/smart-contract-platform/list-contracts). ## Run in Postman To use the Postman collection, select **Run in Postman** below. You can fork the collection to your workspace, view the collection in the public workspace, or import the collection into Postman. * **Fork**: Creates a copy of the collection while maintaining a link to the parent. * **View**: Allows you to try out the API without importing anything into your Postman suite. * **Import**: Creates a copy of the collection but does not maintain a link to Circle's copy. | Collection | Link | | :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Contracts | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/21445022-20c27ad9-62c1-4c95-8adc-e2d2a4a473cd?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-20c27ad9-62c1-4c95-8adc-e2d2a4a473cd%26entityType%3Dcollection%26workspaceId%3D73acd722-fab9-49b0-9382-086659476258) | ## Authorization Paste your [API key](/contracts/create-api-key) in the Authorization tab of the collection. To store your API key as a variable for more advanced testing, see Postman's [using variables](https://learning.postman.com/docs/sending-requests/variables/) guide. # Quickstart: Deploy a Smart Contract using Bytecode Source: https://developers.circle.com/contracts/scp-deploy-smart-contract Deploy smart contract bytecode using Circle Contracts This quickstart walks you through deploying a smart contract using the compiled bytecode and ABI using Circle Contracts. Circle Contracts provides an API for deploying, exploring, and interacting with smart contracts. The platform offers a powerful toolset for developers to build decentralized applications and for businesses to transition onchain. This guide can also be followed to deploy smart contracts on the other [supported blockchains](/contracts/supported-blockchains) by changing the `blockchain` parameter in your request. Additionally, you can deploy to Mainnet by swapping out the Testnet API key for a Mainnet API key. See the [Testnet vs Mainnet](/circle-mint/references/sandbox-and-testing#transition-to-production) guide for more details. ## **Prerequisites** Before you begin, ensure you've: 1. Created [an API key in the Circle Console](/contracts/create-api-key). 2. [Registered your Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret). ## **Step 1: Project setup** Set up your local development environment and install the required dependencies. ### 1.1 Set up a new project Create a new directory, navigate to it and initialize a new project with de1ault settings ```shell NodeJS theme={null} mkdir scp-bytecode-deploy cd scp-bytecode-deploy npm init -y npm pkg set type=module ``` ```shell Python theme={null} mkdir scp-bytecode-deploy cd scp-bytecode-deploy python3 -m venv .venv source .venv/bin/activate ``` ### 1.2 Install dependencies In the project directory, install the required dependencies. This guide uses SDKs for Circle [developer-controlled wallets](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet) and [Contracts](https://developers.circle.com/contracts). ```ts NodeJS theme={null} npm install @circle-fin/developer-controlled-wallets @circle-fin/smart-contract-platform ``` ```py Python theme={null} pip install circle-smart-contract-platform circle-developer-controlled-wallets ``` ## Step 2: Create a wallet and fund it with testnet tokens In this section, you will create a developer-controlled wallet with the SDK and fund it with testnet USDC to pay for the gas fees needed to deploy the smart contract. If you already have a developer-controlled wallet, skip to [Step 3](#step-3:-compile-a-smart-contract). ### 2.1 Setup and run a create-wallet script Import the developer-controlled wallets SDK and initialize the client. You will require your API key and Entity Secret for this. Note that your API key and Entity Secret are sensitive credentials. Do not commit them to Git or share them publicly. Store them securely in environment variables or a secrets manager.\ Developer-controlled wallets are created in a wallet set, which is the source from which individual wallet keys are derived. ```typescript NodeJS theme={null} import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; const client = initiateDeveloperControlledWalletsClient({ apiKey: "", entitySecret: "", }); // Create a wallet set const walletSetResponse = await client.createWalletSet({ name: "WalletSet 1", }); console.log("Created WalletSet", walletSetResponse.data?.walletSet); // Create a wallet on Arc Testnet const walletsResponse = await client.createWallets({ blockchains: ["ARC-TESTNET"], count: 1, walletSetId: walletSetResponse.data?.walletSet?.id ?? "", }); console.log("Created Wallets", walletsResponse.data?.wallets); ``` ```python Python theme={null} from circle.web3 import utils from circle.web3 import developer_controlled_wallets client = utils.init_developer_controlled_wallets_client( api_key="", entity_secret="" ) wallet_sets_api = developer_controlled_wallets.WalletSetsApi(client) wallets_api = developer_controlled_wallets.WalletsApi(client) # Create a wallet set wallet_set = wallet_sets_api.create_wallet_set( developer_controlled_wallets.CreateWalletSetRequest.from_dict({ "name": "Wallet Set 1" }) ) # Create a wallet on Arc Testnet wallet = wallets_api.create_wallet( developer_controlled_wallets.CreateWalletRequest.from_dict({ "blockchains": ["ARC-TESTNET"], "count": 1, "walletSetId": wallet_set.data.wallet_set.actual_instance.id }) ) ``` If you are not using the developer-controlled wallets SDK, you can call the API directly as well. You will need to make 2 requests, one to create the wallet set and one to create the wallet. Make sure to replace the [entity secret ciphertext](https://developers.circle.com/wallets/dev-controlled/entity-secret-management#entity-secret-ciphertext) and idempotency key. ```shell theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/developer/walletSets \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "entitySecretCiphertext": "", "idempotencyKey": "", "name": "WalletSet 1" } ' ``` The wallet set ID is required for creating the wallet. ```shell theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/developer/wallets \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "blockchains": [ "ARC-TESTNET" ], "entitySecretCiphertext": "", "walletSetId": "", "accountType": "EOA", "count": 1, ] } ' ``` You should end up with a new developer-controlled wallet, and the response will look something like this: ```json theme={null} [ { "id": "a2f67c91-b7e3-5df4-9c8e-42bbd51a9fcb", "state": "LIVE", "walletSetId": "5c3e9f20-8d4b-55a1-a63b-c21f44de8a72", "custodyType": "DEVELOPER", "refId": "", "name": "", "address": "0x9eab451f27dca39bd3f5d76ef28c86cc0b3a72df", "blockchain": "ARC-TESTNET", "accountType": "EOA", "updateDate": "2025-11-07T01:35:03Z", "createDate": "2025-11-07T01:35:03Z" } ] ``` ### 2.3 Fund the wallet with test USDC Obtain some testnet USDC for executing transactions like making transfers and paying gas fees for those transactions. Circle's [Testnet Faucet](https://faucet.circle.com/) provides testnet USDC and can be used once per hour to obtain additional USDC. ### 2.4 Check the wallet's balance You can check your wallet's balance from the [Developer Console](https://console.circle.com/wallets/dev/wallets) or programmatically by making a request to [`GET /wallets/{id}/balances`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-balance) with the wallet ID of the wallet you created. ```ts NodeJS theme={null} const response = await client.getWalletTokenBalance({ id: "", }); ``` ```py Python theme={null} try: wallet_balance = wallets_api.list_wallet_balance(id="") print(wallet_balance.json()) except developer_controlled_wallets.ApiException as e: print("Exception when calling WalletsApi->list_wallet_balance: %s\n" % e) ``` ```shell cURL theme={null} curl --request GET \ --url 'https://api.circle.com/v1/w3s/wallets/{}/balances' \ --header 'accept: application/json' \ --header 'authorization: Bearer ' ``` ## **Step 3: Compile a smart contract** In this section, you will compile and deploy a minimal smart contract for an onchain payment inbox using Contracts. Users pay by approving and depositing USDC, payments are recorded via events, and the owner can withdraw the accumulated balance. This contract is intentionally minimal and for learning purposes only. Smart contracts that manage real funds typically require additional security patterns, testing, and audits, and often rely on community-reviewed libraries such as [OpenZeppelin](https://www.openzeppelin.com/). ```solidity theme={null} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } contract MerchantTreasuryUSDC { address public immutable owner; IERC20 public immutable usdc; event PaymentReceived(address indexed sender, uint256 amount); event FundsWithdrawn(address indexed to, uint256 amount); constructor(address _owner, address _usdc) { require(_owner != address(0), "Invalid owner"); require(_usdc != address(0), "Invalid USDC"); owner = _owner; usdc = IERC20(_usdc); } function deposit(uint256 amount) external { require(amount > 0, "Invalid amount"); bool ok = usdc.transferFrom(msg.sender, address(this), amount); require(ok, "USDC transferFrom failed"); emit PaymentReceived(msg.sender, amount); } function withdraw() external { require(msg.sender == owner, "Unauthorized"); uint256 amount = usdc.balanceOf(address(this)); require(amount > 0, "No funds"); bool ok = usdc.transfer(owner, amount); require(ok, "USDC transfer failed"); emit FundsWithdrawn(owner, amount); } function balance() external view returns (uint256) { return usdc.balanceOf(address(this)); } } ``` * `constructor(address _owner)`: Sets the treasury owner and the USDC token address at deployment * `receive() (payable)`: Transfers approved USDC from the caller into the contract and emits `PaymentReceived(sender, amount)` * `withdraw()`: Allows only the owner to withdraw the entire USDC balance; emits `FundsWithdrawn(to, amount)` * `balance() (view)`: Returns the contract's current USDC balance ### 3.1 Obtain ABI and bytecode from Remix IDE 1. Open the [Remix IDE](https://remix.ethereum.org/). 2. Create a new file in the contracts folder called `MerchantTreasury.sol`. 3. Copy and paste the Solidity code into the file, then click on the Compile button. 4. Navigate to the Solidity Compiler tab from the left sidebar. Under Contracts, make sure MerchantTreasuryUSDC (Merchant Treasury.sol) is selected. You should see the option to copy the ABI and Bytecode. These values will be used in the next step. 1. The compiler output is available under Compilation Details. For more information on the Solidity compiler's outputs, see [using the compiler](https://docs.soliditylang.org/en/stable/using-the-compiler.html). 2. The Application Binary Interface (ABI) is the standard way to interact with contracts on an EVM from outside the blockchain and for contract-to-contract interaction. ## Step 4: Deploy the smart contract In this section, you will deploy the smart contract on Arc using the contract's ABI and bytecode, which you have compiled in the previous step. Import and initialize the Contracts SDK, then copy the ABI JSON and raw bytecode over from Remix. Note that you need to append `0x` to the raw bytecode. The `constructorParameters` correspond to the arguments encoded in the contract's deployment bytecode. Since different contracts define different constructors, these parameters vary based on the specific contract being deployed. For this contract, the parameters are the wallet address of the owner and the USDC token contract address on Arc Testnet. ```typescript NodeJS theme={null} import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform"; const client = initiateSmartContractPlatformClient({ apiKey: "", entitySecret: "", }); const abiJson = PASTE_YOUR_ABI_JSON_HERE; const bytecode = "0xPASTE_YOUR_BYTECODE_HERE"; const response = await client.deployContract({ name: "MerchantTreasury Contract", description: "Contract to receive payments and allow an owner to withdraw funds", blockchain: "ARC-TESTNET", walletId: "", abiJson: JSON.stringify(abiJson, null, 2), bytecode: bytecode, constructorParameters: [ "", // Initial owner of the contract "0x3600000000000000000000000000000000000000", // USDC contract address on Arc Testnet ], fee: { type: "level", config: { feeLevel: "MEDIUM" } }, }); console.log(response.data); ``` ```python Python theme={null} from circle.web3 import smart_contract_platform from circle.web3 import utils client = utils.init_smart_contract_platform_client( api_key="", entity_secret="" ) api_instance = smart_contract_platform.DeployImportApi(client) abi_json_str = """PASTE_YOUR_ABI_JSON_HERE""" abi = json.loads(abi_json_str) abi_json = json.dumps(abi) request = smart_contract_platform.ContractDeploymentRequest.from_dict({ "name": 'MerchantTreasury Contract', "description": 'Contract to receive payments and allow an owner to withdraw funds', "blockchain": 'ARC-TESTNET', "walletId": '', "abiJson": abi_json, "bytecode": "0xPASTE_YOUR_BYTECODE_HERE", "constructorParameters": ['', '0x360000000000000000000000000000000000000'], # owner address and USDC contract address on Arc Testnet "feeLevel": 'MEDIUM', }) response = api_instance.deploy_contract( contract_deployment_request=request ) print(response.json()) ``` ```shell cURL theme={null} curl --request POST \ --url https://api.circle.com/v1/w3s/contracts/deploy \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "idempotencyKey": "", "name": "MerchantTreasury Contract", "description": "Contract to receive payments and allow an owner to withdraw funds", "walletId": "", "blockchain": "ARC-TESTNET", "abiJson": "PASTE_YOUR_ABI_JSON_HERE", "bytecode": "0xPASTE_YOUR_BYTECODE_HERE", "constructorParameters": ["", "0x36000000000000000000000000000000000000"], "feeLevel": "MEDIUM", "entitySecretCiphertext": "" } ' ``` After running the script successfully, you should receive a response object that looks like this: ```shell theme={null} { contractId: 'xxxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx', transactionId: 'xxxxxxxx-xxxx-5xxx-axxx-xxxxxxxxxxxx' } ``` You can check the status of the deployment from the [Developer Console](https://console.circle.com/smart-contracts/contracts) or run `getContract` from the SDK directly. ```ts NodeJS theme={null} const response = await circleContractSdk.getContract({ id: "", }); ``` ```py Python theme={null} api_instance = smart_contract_platform.ViewUpdateApi(client) response = api_instance.get_contract(id="") print(response.json()) ``` ```shell cURL theme={null} curl --request GET \ --url https://api.circle.com/v1/w3s/contracts/{CONTRACT_ID} \ --header 'Authorization: Bearer ' ``` Once your contract is deployed, you will be able to interact with it from your application and mint new NFTs with it. You should be able to see it from the console and on the [Arc Testnet Explorer](https://testnet.arcscan.app/). # Quickstart: Event Monitoring for Smart Contracts Source: https://developers.circle.com/contracts/scp-event-monitoring In this guide you'll set up real-time push notifications for specific Events that occur in your smart contracts. You can then use those events to trigger important functionality in your application. ## Prerequisites Before you begin: * [Create an API key](/contracts/create-api-key) in the Circle Console Perform the steps below: 1. [Step 1. Configure Your Webhook for Notifications](/contracts/scp-event-monitoring#step-1-configure-your-webhook-for-notifications) 2. [Step 2. Import Your Smart Contract](/contracts/scp-event-monitoring#step-2-import-your-smart-contract) 3. [Step 3. Create an Event Monitor](/contracts/scp-event-monitoring#step-3-create-an-event-monitor) 4. [Step 4. Fetch Event History](/contracts/scp-event-monitoring#step-4-fetch-event-history) ### Step 1. Configure Your Webhook for Notifications To receive notifications from Circle, you must expose a publicly accessible subscriber endpoint on your side. This endpoint should handle POST requests over HTTPS. For more information on setting up a webhook, refer to the [Set Up Webhooks](/wallets/webhook-notifications), and optionally watch the following video about Webhook Configurations.