# 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 marketplace Source: https://developers.circle.com/agent-stack/agent-marketplace A curated, compliance-first catalog of x402 services that accept USDC, built for AI agents to discover and pay per request. The Agent Marketplace is a curated catalog of [x402](/gateway/nanopayments/concepts/x402)-compatible services that accept USDC. Every listing is screened (each seller's payout wallet is continuously sanctions-checked) and continuously health-checked, so your agent finds services that are compliant and live. Buyers and sellers meet here. Buyers (AI agents) find services and pay per request, with no subscriptions, no accounts, and no API keys. Sellers (API owners) list a service once and get paid in USDC for each call. Browse the catalog at [agents.circle.com/services](https://agents.circle.com/services), or query it programmatically with the [Discovery API](/agent-stack/agent-marketplace/discovery-api). ## Explore Query the public catalog over one HTTP endpoint. Filter by network, price, payment rail, and protocol type. No API key. Monetize your API by charging agents per request in USDC. Scaffold it in minutes with a Circle Skill. Submit your service for review. Once approved, it appears in the catalog and the Discovery API. Explore 600+ live services across 15+ blockchain networks in your browser. ## How a paid request works Every service in the catalog follows the same [x402](/gateway/nanopayments/concepts/x402) handshake: 1. An agent calls the service with no payment. The service responds `402 Payment Required`, plus a machine-readable price list (the `accepts` array). Each entry states which token (`asset`), on which blockchain (`network`), how much (`amount`), and to which address (`payTo`). 2. The agent's wallet picks an option it can pay, signs a USDC payment, and retries the request with the signed payment attached. 3. The service settles the payment and returns the real response. No accounts, no API keys: the payment is the authentication. ## Why the Agent Marketplace * **One integration, many blockchains**: A single query returns services priced across 15+ networks. No per-chain registry hunting. * **Curated, not scraped**: Filter 600+ services by query, network, price, payment rail, and protocol type, instead of parsing thousands of raw endpoints. * **Compliance built in**: Each seller's payout wallet is continuously sanctions-screened. Blocked sellers drop out, so you inherit a watchlist-clean catalog. * **Agent-native responses**: Each listing ships structured payment requirements plus input and output JSON Schema, so any wallet can construct and execute payment without a human reading docs. * **Live, not stale**: Endpoints are continuously health-checked, and unreachable ones are auto-excluded. * **Standards-based**: CAIP-2 networks, the x402 protocol, the [x402 Bazaar](https://docs.x402.org/extensions/bazaar) discovery schema, and OpenAPI and A2A discovery surfaces. No API key, no account. # How-to: Become a seller Source: https://developers.circle.com/agent-stack/agent-marketplace/become-a-seller Monetize your API by charging AI agents per request in USDC, with no API keys and no invoices. Make your API charge AI agents per request in USDC over [x402](/gateway/nanopayments/concepts/x402): your endpoint returns `402 Payment Required` when a request is unpaid, and serves the resource when a valid payment is attached. There are no API keys to issue, no invoices to send, and no accounts to manage. Follow these steps to take an Express API from zero to paid and ready to [list in the Agent Marketplace](/agent-stack/agent-marketplace/get-listed). The `accept-agent-payments` [Circle Skill](/ai/skills) scaffolds these steps directly in your codebase. Install it in your AI IDE with `circle skill install --tool claude-code --name accept-agent-payments` (or see the other install options on the [skills page](/ai/skills)), then ask your agent to add agent payments to your service. ## Prerequisites Before you begin, ensure that you've: * Obtained an EVM wallet address to receive USDC. Buyers pay to this address, and only you control it. If you don't have one, create an [agent wallet](/agent-stack/agent-wallets/quickstart) with Circle CLI. * Built an HTTP API that you can add payment middleware to. * Installed [Node.js](https://nodejs.org/) v22.6+. * Chosen a price per request (for example, `$0.01`; sub-cent prices work too). ## Steps ### Step 1. Install the SDK ```shell theme={null} npm install @circle-fin/x402-batching @x402/core @x402/evm viem express ``` ### Step 2. Return 402 and settle payments Add the Gateway middleware and put a price on a route. Set `sellerAddress` to your payout wallet address; buyers see it as the `payTo` address in the price list your endpoint returns: ```ts server.ts theme={null} import express from "express"; import { createGatewayMiddleware } from "@circle-fin/x402-batching/server"; const app = express(); const gateway = createGatewayMiddleware({ sellerAddress: "0xYOUR_WALLET_ADDRESS", // your payout wallet facilitatorUrl: "https://gateway-api-testnet.circle.com", // testnet }); app.get("/premium-data", gateway.require("$0.01"), (req, res) => { res.json({ data: "Your paid content" }); }); app.listen(3000); ``` `gateway.require("$0.01")` returns `402 Payment Required` with the payment options for unpaid requests, and settles valid payments with Gateway before your handler runs. Full walkthrough: [Nanopayments seller quickstart](/gateway/nanopayments/quickstarts/seller). ### Step 3. Accept vanilla x402 too The preceding middleware accepts [Gateway nanopayments](/agent-stack/agent-nanopayments): gasless, sub-cent payments settled offchain in batches. To also accept onchain x402 payments and reach buyers on non-Circle x402 stacks, run an `x402ResourceServer` with an x402 facilitator alongside the Gateway scheme: ```ts theme={null} import { x402ResourceServer } from "@x402/express"; import { HTTPFacilitatorClient } from "@x402/core/server"; import { BatchFacilitatorClient, GatewayEvmScheme, } from "@circle-fin/x402-batching/server"; const server = new x402ResourceServer([ new HTTPFacilitatorClient({ url: "https://facilitator.example.com" }), new BatchFacilitatorClient(), ]); server.register("eip155:*", new GatewayEvmScheme()); await server.initialize(); ``` `GatewayEvmScheme` extends the standard onchain scheme, so your `402` responses offer both rails in one `accepts` array and buyers pick whichever they have funded. Details: [Add nanopayments to an x402 seller](/gateway/nanopayments/howtos/x402-seller). Supporting both rails maximizes the buyers who can transact with you, the same way a merchant accepts more than one card network. For the same reason, accept payment on more than one blockchain (for example, Base and Polygon PoS): a buyer can only pay from a blockchain where you accept it. ### Step 4. Test the handshake Send an unpaid request and confirm the `402`: ```shell theme={null} curl -i http://localhost:3000/premium-data ``` You should see `402 Payment Required` with a `PAYMENT-REQUIRED` header that carries the payment options. Then pay it end to end with the [buyer quickstart](/gateway/nanopayments/quickstarts/buyer) client or the Circle CLI [pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service) flow. ### Step 5. Check earnings and withdraw Gateway payments accumulate in your Gateway balance and batch-settle onchain. Check and withdraw with `GatewayClient`: ```ts theme={null} import { GatewayClient } from "@circle-fin/x402-batching/client"; const client = new GatewayClient({ chain: "arcTestnet", privateKey: process.env.PRIVATE_KEY as `0x${string}`, }); const balances = await client.getBalances(); console.log(`Available: ${balances.gateway.formattedAvailable} USDC`); await client.withdraw("50"); ``` ## See also * [How-to: Get listed](/agent-stack/agent-marketplace/get-listed): put your live endpoint in the marketplace catalog. * [Seller integration tools](/agent-stack/agent-nanopayments/seller-integration-tools): third-party platforms that run the payment layer for you if you are not on Express. * [What is x402?](/gateway/nanopayments/concepts/x402): the payment handshake behind these steps. # Discover x402 services Source: https://developers.circle.com/agent-stack/agent-marketplace/discovery-api openapi/agent-marketplace-discovery.yaml get /v2/x402/discovery/resources One public endpoint to discover x402 services that accept USDC, across many blockchains. No API key, no account. ## Categories Every listing is classified with one category in `metadata.provider.category`. Filter on it with the `category` query parameter to narrow results to a domain. | Category | Description | | :-------------------- | :------------------------------------------------------ | | `SOCIAL_INTELLIGENCE` | Social platform data and audience or sentiment signals. | | `FINANCIAL_ANALYSIS` | Market data, prices, and financial analytics. | | `WEB_SEARCH_RESEARCH` | Web search, scraping, and research retrieval. | | `PREDICTION_MARKETS` | Odds, forecasts, and prediction-market data. | | `CREATIVE` | Media generation such as images, audio, and text. | | `INFRASTRUCTURE` | Developer tooling and infrastructure utilities. | ## Paying for a service Discovery tells you what each service accepts. To actually pay, use one of two rails: * **Vanilla x402**: a signed onchain USDC transfer that any USDC wallet, including a centralized custodial one, can produce. * **Circle Gateway**: offchain, batched settlement for gasless, sub-cent payments. Each listing's `accepts[]` and its `supportsVanillax402` and `supportsCircleGateway` flags tell you which rails it takes. To pay from Circle CLI, see [Pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service). For the concepts and buyer integration, see [Agent nanopayments](/agent-stack/agent-nanopayments). To find services payable on a specific blockchain, filter with the structured `network` parameter, not the `query` text field. `query` matches free text (URLs, providers, descriptions, tags), so it does not reliably match a chain. ## Related * [Service catalog](https://agents.circle.com/services) * [OpenAPI spec](https://agents.circle.com/.well-known/openapi.json) * [A2A card](https://agents.circle.com/.well-known/a2a.json) * [Agent skills index (llms.txt)](https://agents.circle.com/llms.txt) # How-to: Get listed Source: https://developers.circle.com/agent-stack/agent-marketplace/get-listed Submit your x402 service for review to appear in the Agent Marketplace catalog and the Discovery API. List your service in the [Agent Marketplace](/agent-stack/agent-marketplace) so agents can discover and pay for it. Approved listings appear in both the [catalog UI](https://agents.circle.com/services) and the [Discovery API](/agent-stack/agent-marketplace/discovery-api), where agents filter by category, network, and price. Listings are reviewed and approved manually today. A self-serve, automated submission flow is coming. For now, submit the form below and the team reviews your service. ## Prerequisites Before you begin, ensure that you've: * Made your service payable: it returns `402 Payment Required` when a request is unpaid and serves the resource when paid. See [How-to: Become a seller](/agent-stack/agent-marketplace/become-a-seller). * Published an OpenAPI spec for your service, so agents can read its inputs and outputs. * Confirmed the payout wallet address your service uses. You submit it with the form, and it is sanctions-screened during review. ## Steps Submit your service details at the [intake form](https://forms.gle/7YFzvdmMcn1JH5tF6). Include your endpoint URL, payout wallet address, and a short description. The team screens and approves your submission. Your seller payout wallet is sanctions-screened as part of review. Once approved, your service appears in the catalog and is returned by the Discovery API. It is then continuously health-checked, so it stays listed only while it is reachable. Agents find you through the [Discovery API](/agent-stack/agent-marketplace/discovery-api) filters, so make sure your metadata makes your service easy to find. # 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. # Seller integration tools Source: https://developers.circle.com/agent-stack/agent-nanopayments/seller-integration-tools Third-party platforms that help API providers accept x402 and nanopayments. If you run an API or online service, you can charge AI agents per request by accepting [x402](/gateway/nanopayments/concepts/x402) payments and [nanopayments](/agent-stack/agent-nanopayments). The third-party platforms below help you add this support without building the integration yourself. To integrate nanopayments directly into your own server, follow the [seller quickstart](/gateway/nanopayments/quickstarts/seller) or the [how-to for adding nanopayments to an existing x402 seller](/gateway/nanopayments/howtos/x402-seller). ## Third-party platforms These independent platforms offer hosted tooling and onboarding flows for accepting agent payments. Supported payment standards and blockchains vary by platform, so check each platform for details. ### Blockrun [Blockrun](https://blockrun.ai) supports x402 and nanopayments. It helps API sellers list their services in a directory and accept payments settled in USDC onchain. ### Proceeds [Proceeds](https://myproceeds.xyz) supports x402, nanopayments, and the Machine Payments Protocol (MPP). It helps sellers accept agent payments on Arc and other blockchains. ### Sponge [Sponge](https://paysponge.com) supports x402 and MPP, but not nanopayments. It lets API providers accept agent payments with minimal code changes. ### x402scan [x402scan](https://www.x402scan.com/) by Merit Systems is a registry for x402 and agent-native APIs. It helps providers make their services discoverable to AI agents and accept onchain USDC payments for each request. # 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} Run curl -sL https://agents.circle.com/skills/setup.md, and use the returned setup instructions to set up my 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. If your agent stalls waiting for an email verification prompt, use the [non-interactive authentication flow](/agent-stack/agent-wallets/wallet-operations/authenticate#non-interactive-scripts-and-ai-agents), which is built for scripts and AI agents that can't respond to interactive prompts. ## 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.05 | 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 using Circle CLI. Prefer a guided setup? Paste this prompt into your AI agent instead: ```text theme={null} Run curl -sL https://agents.circle.com/skills/setup.md, and use the returned setup instructions to set up my agent wallet. ``` 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`. On testnet, `circle wallet fund` draws 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. Session secrets are stored in your operating system's secure keychain. Run `circle wallet logout` to clear the session. 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`. On testnet, `circle wallet fund` draws 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\|table` | Set output format. Defaults to `table`. | | `-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. Only `agent` is supported (the default). | | `--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` 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`, `eurc`, `native`. | | `--method` | Funding method: `crypto` or `fiat`. Required on mainnet. Omit on testnet, where `circle wallet fund` draws 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: draws from the Circle 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%). Defaults to `300` (3%). | | `--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 ``` # 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. ### `accept-agent-payments` Monetize an API by charging AI agents per request in USDC over [x402](/gateway/nanopayments/concepts/x402). Scaffolds both payment rails (Gateway nanopayments and vanilla x402) and prepares your service for the [Agent Marketplace](/agent-stack/agent-marketplace). ### `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 Find the machine-readable OpenAPI specification for every Circle API ## 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 # Audit Logs overview Source: https://developers.circle.com/api-reference/audit-logs Understand what the Audit Logs API records, who can call it, and how action events differ from state-change events. The Audit Logs API returns an immutable record of security-relevant actions taken in your Circle entity, such as user sign-ins, approval requests, and policy changes. Use it to review who did what, when, and from where. ## Availability The Audit Logs API is available across all Circle products. Call it with the API key from whichever Circle product you're integrating with. ## Retrieve audit log events You can retrieve audit log events with two endpoints: * [List audit logs](/api-reference/audit-logs/list-audit-logs) returns a paginated, newest-first list, filterable by time range, event type, actor, and outcome. * [Get an audit log by ID](/api-reference/audit-logs/get-audit-log-by-id) returns a single audit log event by its `id`. ## Actor types Every audit log event identifies an actor with `actorId` and `actorType`. The actor type tells you what kind of identifier `actorId` is. | Actor type | What it represents | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `USER` | A human who performed the action through the [Circle Console](https://console.circle.com/), [Mint Console](https://app.circle.com/), or [CPN Console](https://cpn.circle.com/). `actorId` is the user's identifier. | | `API_KEY` | A Circle API key that performed the action programmatically. `actorId` is the API key's identifier. | | `SYSTEM` | Circle-internal automation that performed the action on your entity's behalf, such as expiring an approval request on its scheduled deadline. | ## Action events and state-change events Every audit log event is either an action event or a state-change event. The `type` field tells you which, and determines which fields carry the event's details. * **Action events** record that something happened, with no prior or new resource state to compare. * **State-change events** record a change to a resource in your entity, such as a policy update. Both action and state-change events share a common set of fields that identify who did what and when, such as `id`, `type`, `occurredAt`, or `actorId`. For action events, additional fields specific to that event appear on `payload`. State-change events populate `oldValues` (prior state) and `newValues` (new state); `payload` might also carry additional context. ### Example action event A `user.signed_in` event is an action event. It carries the client's user agent on `payload`: ```json theme={null} { "id": "abcdef01-2345-4678-9abc-def012345678", "type": "user.signed_in", "occurredAt": "2026-07-14T15:58:27.093995Z", "actorId": "example-user-id", "actorType": "USER", "clientIp": "203.0.113.42/32", "outcomeResult": "SUCCESS", "entityId": "deadbeef-1234-4567-89ab-cdef01234567", "requestId": "example-request-id-001", "payload": { "user_agent": "Mozilla/5.0 ..." } } ``` ### Example state-change event A `policy.applied` event is a state-change event. It carries `oldValues` and `newValues` alongside `payload`: ```json theme={null} { "id": "01234567-89ab-4cde-8f01-234567890abc", "type": "policy.applied", "occurredAt": "2026-07-14T16:02:11.842Z", "actorId": "example-user-id", "actorType": "USER", "outcomeResult": "SUCCESS", "entityId": "deadbeef-1234-4567-89ab-cdef01234567", "requestId": "example-request-id-002", "oldValues": { "status": "inactive" }, "newValues": { "status": "active" }, "payload": { "action": "policy_change", "operation": "ACTIVATE", "scope": "RESOURCE" } } ``` ## Audit log event types The following event types are recorded in the audit log. The list grows over time as new products and controls add events. Your integration should tolerate unknown `type` values and unknown keys inside `payload`. ### Sign-in events Action events recorded when a user signs in to the [Circle Console](https://console.circle.com/), [Mint Console](https://app.circle.com/), or [CPN Console](https://cpn.circle.com/). Actor is `USER` (see the [example action event](#example-action-event)). | Event type | Recorded when | | ---------------- | ------------------------------------- | | `user.signed_in` | A user signed in to a Circle console. | Each event's `payload` might carry the following fields: | Field | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------ | | `email` | string | No | Email address of the user who signed in, if available. | | `user_agent` | string | No | `User-Agent` header of the authenticating client. | ### Policy Engine events Circle's Policy Engine records an immutable event each time an approval request is resolved or a policy changes state. See [Policy configuration, permissions, and approvals setup](https://help.circle.com/s/article/Policy-configuration-permissions-and-approvals-setup) to set up policies and approval rules. #### Approval request events Action events recorded when an approval request is resolved. Actor is `USER` (see the [example action event](#example-action-event)). | Event type | Recorded when | | -------------------- | ----------------------------------------------------------------------------------------------------------------- | | `proposal.approved` | The request received the required approvals; the action is authorized to proceed. | | `proposal.rejected` | An approver rejected the request. | | `proposal.expired` | The request reached its expiration window without completing the required approvals and was closed automatically. | | `proposal.cancelled` | The initiator withdrew their own pending request before a decision was reached. | Each event's `payload` might carry the following fields: | Field | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------- | | `action` | string | Yes | The governed action the proposal concerns. | | `kind` | string | No | Proposal kind: `PROPOSAL`, `POLICY_CHANGE`, or `LIST_CHANGE`. | | `policy_id` | string | No | Identifier of the related policy. | #### Policy change events State-change events recorded across the lifecycle of a policy change. Actor is `USER` or `SYSTEM`. These populate `oldValues` and `newValues` (see the [example state-change event](#example-state-change-event)). | Event type | Recorded when | | ------------------------- | ---------------------------------------------------------------------------- | | `policy.change_requested` | A policy create, revision, or delete was submitted and is awaiting approval. | | `policy.applied` | An approved policy change took effect. | Each event's `payload` might carry the following fields: | Field | Type | Required | Description | | --------------------- | ------ | -------- | -------------------------------------------------------- | | `action` | string | Yes | The policy action being performed. | | `operation` | string | Yes | Operation applied: `ACTIVATE`, `SUPERSEDE`, or `DELETE`. | | `scope` | string | Yes | Scope: `RESOURCE`, `GROUP`, or `WILDCARD`. | | `policy_lookup_key` | string | No | Lookup key of the affected policy. | | `previous_version_id` | string | No | Identifier of the prior policy version. | | `proposal_id` | string | No | Identifier of the driving proposal. | # Get an audit log by ID Source: https://developers.circle.com/api-reference/audit-logs/get-audit-log-by-id openapi/openapi-public.yaml get /v1/auditLogs/{id} Returns a single audit log entry by its unique identifier. # List audit logs Source: https://developers.circle.com/api-reference/audit-logs/list-audit-logs openapi/openapi-public.yaml get /v1/auditLogs Returns a paginated list of audit log entries for your account, ordered from newest to oldest. Use the query parameters to filter by time range, event type, actor, or outcome. Results are paginated using opaque cursors returned in the `Link` response header. # CCTP API Source: https://developers.circle.com/api-reference/cctp Fetch attestations and support native USDC transfers across blockchains using Cross-Chain Transfer Protocol. CCTP is permissionless and open to any wallet. Burn USDC on a source blockchain, then mint it on the destination once Circle issues the corresponding attestation. No API key required. ## Get started Learn how Cross-Chain Transfer Protocol works. See which blockchains CCTP supports and the domain IDs you'll need. ## Endpoint categories Current endpoints for messages, attestations, and burn fees. Legacy endpoints for existing V1 integrations. # 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. # Circle Mint API Source: https://developers.circle.com/api-reference/circle-mint Manage USDC and EURC balances, process crypto deposits and payouts, execute cross-currency trades, and manage reserves. Use Circle Mint if you're an institutional customer—typically an exchange, fintech, bank, or trading firm—working directly with Circle to mint and redeem stablecoins. ## Get started Authenticate your requests with an API key. Try the Circle Mint API with Circle's Postman collection. Set up webhook subscriptions for Circle Mint events. ## Endpoint categories Manage balances, wires, transfers, payouts, deposits, and addresses. Receive stablecoin payments and manage payment intents. Send stablecoin payouts and manage the address book. Quote, trade, and settle between currencies. Access credit lines, transfers, fees, and repayments. Report daily custody balances for reserve management. ## OpenAPI specifications The Circle Mint API reference is generated from these OpenAPI specifications: * `https://developers.circle.com/openapi/account.yaml` * `https://developers.circle.com/openapi/general.yaml` * `https://developers.circle.com/openapi/institutional.yaml` * `https://developers.circle.com/openapi/payments.yaml` * `https://developers.circle.com/openapi/payouts.yaml` * `https://developers.circle.com/openapi/cross-currency.yaml` * `https://developers.circle.com/openapi/reserve-management.yaml` * `https://developers.circle.com/openapi/credit.yaml` * `https://developers.circle.com/openapi/partner-openapi.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # 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. **cirBTC deposits on the BTC chain:** Requesting a deposit address with `currency=CIRBTC` and `chain=BTC` returns a native Bitcoin (BTC) address. Bitcoin sent to that address is automatically wrapped and credited to the associated wallet as `CIRBTC` (1:1). The returned address itself is a standard BTC address, only the resulting wallet balance is denominated in cirBTC. 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. **cirBTC recipients:** Recipient addresses for cirBTC flows are registered with one of these `chain` and `currency` pairs: - `chain=ETH` with `currency=CIRBTC` for onchain cirBTC transfers. - `chain=BTC` with `currency=BTC` for native Bitcoin recipients. To redeem cirBTC to native Bitcoin, create a transfer with `amount.currency=CIRBTC` to a recipient of this form; Circle will burn the cirBTC and release the equivalent amount of native Bitcoin (1:1) to the Bitcoin 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. **cirBTC transfers:** When `amount.currency=CIRBTC`, the recipient's `chain` (looked up from `destination.addressId`) determines how the transfer is processed: - Recipients on `chain=ETH` receive an onchain cirBTC transfer. - Recipients on `chain=BTC` trigger redemption to native Bitcoin: the cirBTC is burned and an equivalent amount of native Bitcoin (1:1) is released to the Bitcoin address. # 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 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 burn fee calculations, including daily records and surcharges. Returns up to 50 calculations in descending chronological order, or `pageSize` results if specified. # List reports Source: https://developers.circle.com/api-reference/circle-mint/account/list-reports openapi/account.yaml get /v1/reports Returns a paginated list of the account's previously generated reports for a given report type and date range, each with a pre-signed `downloadUrl`. Listable report types: `managed_payout_transactions`, `managed_payment_wallet_balances`, `managed_payin_transactions`, and `camt_managed`. The non-managed `camt053` daily statement is not available through this endpoint; retrieve it with `POST /v1/reports` instead. Note: the `id` of each listed report is a storage object key, not the deterministic report UUID. It cannot be passed to `GET /v1/reports/{id}` or `GET /v1/reports/{id}/content`; download a listed report using its `downloadUrl`. # 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. Supported report types: `camt053`, `camt_managed`, `managed_payout_transactions`, `managed_payment_wallet_balances`, and `managed_payin_transactions`. The managed-payment reports take a `timeframe` field (`daily` or `monthly`); `camt_managed` and `managed_payment_wallet_balances` support `daily` only. 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. # Error codes Source: https://developers.circle.com/api-reference/circle-mint/error-codes Reference for synchronous and asynchronous error codes returned by the Circle Mint API, organized by product, with recommended treatment for each. Circle Mint surfaces errors in two shapes: synchronous responses to API calls, and asynchronous status transitions on resources. The tables below catalog every code, organized by product. When an endpoint returns a generic `code: -1` with a `message` of `Something went wrong`, treat it as the transient fallback and retry with backoff. For general errors that can be returned by any Circle API, see [API errors](/api-reference/errors). ## Error response shape Every synchronous error returns an HTTP status code plus a JSON body with a numeric `code` and a human-readable `message`. Validation errors include an extended `errors[]` array with one entry per offending field. ```json theme={null} { "code": 2, "message": "Invalid entity." } ``` ```json theme={null} { "code": 2, "message": "Invalid entity.", "errors": [ { "error": "min_value", "message": "Must be at least 1.", "location": "amount", "invalidValue": "0", "constraints": { "min": 1 } } ] } ``` Each entry in `errors[]` carries an `error` name that you can branch on programmatically: | Name | Meaning | | ----------------------- | ----------------------------------------------------------- | | `required` | Field is missing. | | `min_value` | Value is below the allowed minimum. | | `max_value` | Value exceeds the allowed maximum. | | `length_outside_bounds` | String length is outside the allowed range. | | `pattern_mismatch` | Value didn't match the required regular expression pattern. | | `date_not_in_past` | Date must be in the past. | | `date_not_in_future` | Date must be in the future. | | `number_format` | Value isn't a valid number for the field. | | `value_must_be_true` | The boolean field must be `true`. | | `value_must_be_false` | The boolean field must be `false`. | | `not_required` | Field was provided but is disallowed. | | `invalid_value` | Value isn't in the allowed set for this field. | ## Synchronous and asynchronous errors Circle Mint surfaces errors in two distinct shapes depending on when the problem is detected. | Kind | Where it surfaces | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Synchronous | Returned inline in the HTTP response with a status code (`400`, `401`, `403`, `404`, `409`, `429`, or `500`) and a JSON body containing `code` and `message`. | | Asynchronous | Surfaced on a resource after it reaches a terminal `failed` (or denied) status. The resource carries a string `errorCode` and, for risk-driven denials, an optional `riskEvaluation` object. | ## Recommended treatment Use these categories to decide how your integration should respond to a given error. | Category | Signal | Action | | ------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Malformed request | 4xx response with a validation `errors[]` array. | Fix the request and retry with the same `idempotencyKey`. | | State conflict | 409 response, or a `code` indicating an existing or conflicting resource. | Re-fetch the resource state and decide whether to retry, update, or skip. | | Insufficient funds | `insufficient_funds` on a payout or transfer, or `INSUFFICIENT_BALANCE` on a credit draw. | Top up the source wallet (or wait for a credit repayment) and retry. | | Compliance or risk denial | Asynchronous `errorCode` paired with `riskEvaluation.decision = denied`. | Review the originator and beneficiary information, then resubmit with a new `idempotencyKey`. | | Transient or network | 5xx response, timeout, or generic `code: -1`. | Retry with exponential backoff. If the error persists, contact Circle. | ## Risk evaluation reason codes When Circle's risk engine acts on a transaction, the affected resource (a payment, payout, or transfer) carries a `riskEvaluation` object alongside its `errorCode`. This object explains why the risk service reached its decision: * `decision`: the risk outcome, one of `approved`, `denied`, or `review`. * `reason`: a numeric reason code, returned as a string such as `"3000"`, that identifies the specific reason for the decision. Every reason code belongs to one of the following categories. The category tells you the source and nature of the block, and the code range narrows it to a specific reason. | Category | Description | Code range | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | Circle blocked | The fiat account or payment contains criteria that Circle can't support, such as a prohibited country. | `3000-3099` | | Processor or issuing bank | Circle's partner processor or the issuing bank can't accept the fiat account or payment, such as an unsupported issuer country. | `3100-3199` | | Regulatory compliance intervention | Circle Risk Service intervened to meet legal and regulatory compliance requirements, such as KYC verification limits. | `3200-3299` | | Fraud risk intervention | Circle Risk Service acted on the transaction because of fraud management issues, such as excessive chargeback rates. | `3300-3499` | | Customer configuration (unsupported) | Circle Risk Service acted on the transaction because of your configuration or request, such as a blocked issuer country or card type. | `3500-3599` | | Customer configuration (fraud) | Circle Risk Service acted on the transaction because of your configuration or request, such as adding a user to a watch list. | `3600-3699` | The following table lists the individual reason codes in each category. | Reason code | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `3000` | Default. | | `3001` | Prohibited issuer (bank) country. | | `3002` | Prohibited billing address country. | | `3020` | Fiat account denied. Check the fiat account for the reason. | | `3022` | Fiat account (card) evaluation timeout. Retry the request. | | `3023` | Fiat account (bank account) evaluation timeout. Retry the request. | | `3026` | Fiat account is in an unverified state. | | `3027` | Fiat account is in a suspended state. | | `3030` | Unsupported bank account routing number (RTN). | | `3040` | Unsupported activity type. | | `3050` | Customer suspended from payment processing. | | `3070` | Transaction exceeds the risk limits. | | `3071` | Daily aggregate limit exceeded (email). | | `3072` | Daily aggregate limit exceeded (fiat). | | `3075` | Weekly aggregate limit exceeded (email). | | `3076` | Weekly aggregate limit exceeded (fiat). | | `3100` | Unsupported return code response from the processor or issuing bank (default). | | `3101` | Invalid return code response from the issuing bank, for example an invalid card. | | `3102` | Fraudulent return code response from the issuing bank, for example a pickup card. | | `3103` | Blocked entity return code response from the processor, for example a blocked card. | | `3104` | Account associated with an invalid ACH RTN. | | `3105` | Expired card. | | `3150` | Administrative return from the ODFI or RDFI. | | `3151` | Return indicating an ineligible account from the customer or RDFI. | | `3152` | Unsupported transaction type return from the customer or RDFI. | | `3200-3202` | Unsupported criteria. | | `3210` | Withdrawal limit exceeded (7-day default payout limit). | | `3211` | Withdrawal limit exceeded (7-day custom payout limit). | | `3220` | Compliance limit exceeded. On Stablecoin Payouts this is a Travel Rule violation; see [Travel Rule Compliance](/circle-mint/references/travel-rule-compliance). | | `3300-3309` | Transaction declined by Circle Risk Service. Contact [risk-investigations@circle.com](mailto:risk-investigations@circle.com) for more information. | | `3310` | The fiat account is directly associated with fraudulent activity. | | `3311` | The email address is directly associated with fraudulent activity. | | `3320` | The fiat account is associated with a network fraud notification. | | `3321` | The email account is associated with a network fraud notification. | | `3330` | The fiat account is flagged by the Risk team. | | `3331` | The email account is flagged by the Risk team. | | `3340` | The fiat account is linked to previous fraudulent activity. | | `3341` | The email address is linked to previous fraudulent activity. | | `3350` | 3DS authentication is required for this transaction. | | `3500` | Default. | | `3501` | Blocked issuer (bank) country. | | `3502` | Blocked billing address country. | | `3520` | Blocked card type, for example credit. | | `3530-3539` | Chargeback history on the Circle platform. | | `3540-3549` | Chargeback history on the customer platform. | | `3550` | Blocked fiat (card). | | `3551` | Blocked email address. | | `3552` | Blocked phone number. | | `3600-3699` | Blocked by a fraud watch list. | ## Common errors These codes can appear on any Circle Mint endpoint. | Code | Meaning | HTTP | Treatment | | ---- | ------------------------------- | ---- | -------------- | | `-1` | Something went wrong. | 500 | Transient | | `1` | Malformed authorization header. | 401 | Malformed | | `2` | Invalid entity. | 400 | Malformed | | `3` | Forbidden. | 403 | State conflict | ## Core API errors These codes cover the core money-movement endpoints: payments, payouts, transfers, and blockchain address management. | Code | Meaning | Treatment | | ------ | --------------------------------------------------------------------------------------------- | ------------------ | | `1077` | Payment amount must be greater than zero. | Malformed | | `1078` | Payment currency not supported. | Malformed | | `1083` | Idempotency key already bound to another request—retry with a different value. | State conflict | | `1084` | This item cannot be canceled. | State conflict | | `1085` | This item cannot be refunded. | State conflict | | `1086` | This payment was already canceled. | State conflict | | `1087` | Total amount to be refunded exceeds the original payment amount. | Malformed | | `1088` | Invalid source account specified in a payout or transfer request. | Malformed | | `1089` | The source account could not be found. | Malformed | | `1093` | Source account has insufficient funds for the payout or transfer amount. | Insufficient funds | | `1096` | Encryption key ID could not be found; an encryption key ID is required for encrypted data. | Malformed | | `1097` | Cannot cancel or refund a failed payment. | State conflict | | `1100` | Invalid country format. | Malformed | | `1101` | Invalid country format—provide a valid ISO 3166-1 alpha-2 country code. | Malformed | | `1106` | Invalid district format—must be a 2-character value. | Malformed | | `1107` | Payout limit exceeded. | State conflict | | `1108` | Country not supported for customer. | Malformed | | `1112` | Country or district error on a request payload. | Malformed | | `2003` | The recipient blockchain address is already associated with the account. | State conflict | | `2004` | The blockchain address is not a verified withdrawal address. | State conflict | | `2005` | The blockchain address belongs to an unsupported blockchain. | Malformed | | `2006` | The wallet type specified when creating an end-user wallet is not supported. | Malformed | | `2007` | A transfer from the provided source to the provided destination is not supported. | Malformed | | `2009` | Unsupported transfer configuration. | Malformed | | `2020` | Unsupported transfer request. | Malformed | | `5001` | Payout doesn't exist—verify the payout ID. | Malformed | | `5002` | Payout amount must be greater than zero. | Malformed | | `5003` | Inactive destination address. Addresses may require a 24-hour wait after creation before use. | State conflict | | `5004` | The destination address for this payout could not be found. | Malformed | | `5005` | The source wallet for this payout could not be found. | Malformed | | `5006` | The source wallet has insufficient funds for this payout. | Insufficient funds | | `5007` | Currency not supported for this operation. | Malformed | | `5011` | Invalid destination address. | Malformed | | `5012` | Cannot search for both crypto and fiat payouts at the same time. | Malformed | | `5013` | Source wallet ID must be a number for payouts search. | Malformed | | `5014` | The blockchain address is not valid for the corresponding blockchain. | Malformed | | `5015` | The destination blockchain doesn't match the currency used. | Malformed | | `5017` | Destination address or blockchain configuration error on a payout request. | Malformed | ## Stablecoin Payins errors Most Stablecoin Payins asynchronous failures surface via the `payments` webhook and the `paymentIntent` lifecycle rather than through discrete error codes. The synchronous codes below cover checkout-session validation. | Code | Meaning | Treatment | | ------ | ------------------------------------------------------------------------ | -------------- | | `1143` | Checkout session not found—the supplied session ID doesn't exist. | Malformed | | `1144` | Checkout session is already in a completed state and cannot be extended. | State conflict | For asynchronous failure events on payment intents and payments, see [Webhook notifications](/circle-mint/references/webhook-notifications). ## Stablecoin Payouts errors Stablecoin Payouts errors have two sources. Synchronous validation covers Travel Rule fields and Address Book entity validation. After submission, the payout entity itself can transition to `failed` with an `errorCode` that describes why. ### Synchronous validation For full Travel Rule context, see [Travel rule compliance](/circle-mint/references/travel-rule-compliance). | Code | Meaning | Treatment | | ------ | -------------------------------------------------------------------------------------- | --------- | | `5020` | `purposeOfTransfer` field is missing or invalid on a `CIRCLE_SG`-booked crypto payout. | Malformed | | `2024` | Address Book identity is missing for this entity. | Malformed | | `2025` | Address Book ownership is missing for this entity. | Malformed | | `2026` | Address Book VASP ID is missing for this entity. | Malformed | | `2027` | Address Book ownership type is invalid for this entity. | Malformed | | `2028` | Address Book custody type is invalid for this entity. | Malformed | | `2029` | Address Book custody is missing for this entity. | Malformed | | `2030` | Address Book VASP ID is invalid for this entity. | Malformed | | `2031` | Address Book identity type is invalid—must be `individual` or `business`. | Malformed | | `2032` | Address Book identity first name is missing for an individual identity. | Malformed | | `2033` | Address Book identity last name is missing for an individual identity. | Malformed | | `2034` | Address Book identity business name is missing for a business identity. | Malformed | | `2035` | Address Book VASP ID is not allowed—VASP ID must be omitted for self-hosted custody. | Malformed | | `2036` | Identity is not allowed in Address Book `PATCH` requests. | Malformed | | `2037` | Ownership is not allowed in Address Book `PATCH` requests. | Malformed | ### Asynchronous entity errors | `errorCode` | Meaning | | ----------------------------- | ----------------------------------------------------------------------------- | | `insufficient_funds` | Source wallet doesn't have enough USDC for the payout. | | `transaction_denied` | Payout was denied by Circle Risk Service—see `riskEvaluation` for the reason. | | `transaction_failed` | Payout failed due to an unknown reason. | | `transaction_returned` | Payout was returned by the receiving network. | | `fiat_account_limit_exceeded` | The fiat account limit was exceeded. | When `riskEvaluation.reason` is `3220`, the denial is a Travel Rule violation. Review your originator identities, beneficiary identity, VASP ID, and `purposeOfTransfer`, then resubmit with a new `idempotencyKey`. See [Travel Rule Compliance](/circle-mint/references/travel-rule-compliance). For the full list of `riskEvaluation.reason` values, see [Risk evaluation reason codes](#risk-evaluation-reason-codes). ## Credit errors The Credit API's draw, fee, and repayment endpoints surface validation failures via a top-level `validationErrors` array on the credit line. When `validationErrors` is non-empty, draw and repayment endpoints return HTTP 400 until the listed conditions clear. | Value | Meaning | Treatment | | ---------------------- | --------------------------------------------------------------------------------------- | -------------- | | `INSUFFICIENT_BALANCE` | The credit line's Circle Mint wallet balance is below `minBalance`, blocking new draws. | State conflict | | `PENDING_FEES` | An unpaid fee is blocking new draws. | State conflict | | `OVERDUE_TRANSFERS` | At least one disbursed transfer is past its due date. | State conflict | Calling `POST /v1/credit/cryptoRepayment` against a Settlement Advance credit line returns HTTP 400—crypto repayment is only supported on Line of Credit lines. See the [Credit API concept](/circle-mint/concepts/credit-api) for product differences. ## Institutional Distribution errors These responses surface when you create or manage external entities through the Institutional API. | HTTP | Meaning | Treatment | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | `400` | Missing or invalid fields on external entity creation. | Malformed | | `401` | The API key doesn't have the Institutional Distribution permission. | State conflict: contact Circle to enable the entitlement. | | `409` | An external entity with the supplied `businessUniqueIdentifier` and `identifierIssuingCountryCode` already exists. Re-fetch via `GET /v1/externalEntities`. | State conflict | ## Custody balance errors These codes apply to daily user balance reporting for custody accounts. | Code | Meaning | Treatment | | -------- | ------------------------------------------------------------------------------------------------------------ | -------------- | | `500000` | Unsupported currency for reporting daily user balance—must be USDC or EURC. | Malformed | | `500001` | Custody balance `asOfDate` is too far from today's date. | Malformed | | `500002` | Custody balance cannot be less than zero. | Malformed | | `500003` | Local custody balance can't be greater than total custody balance. | Malformed | | `500004` | Idempotency key can't be reused across different requests. | State conflict | | `500005` | A custody balance report already exists for the provided date and currency. Contact customer care for edits. | State conflict | ## Transfer entity errors These `errorCode` values appear on a transfer resource after it transitions to `failed`. | `errorCode` | Meaning | Treatment | | -------------------- | -------------------------------------------------------- | --------------------------------------------------------------- | | `transfer_failed` | Transfer could not be completed. | Transient: investigate `riskEvaluation` and network conditions. | | `transfer_denied` | Transfer was denied by Circle Risk Service. | Compliance: review and resubmit with a new `idempotencyKey`. | | `blockchain_error` | Onchain failure prevented the transfer from settling. | Transient: retry once network is healthy. | | `insufficient_funds` | Source wallet doesn't have enough USDC for the transfer. | Insufficient funds | # 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. # Retrieve data for all visible sections Source: https://developers.circle.com/api-reference/circle-mint/onboarding/bulk-get-data openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/data Returns all visible section data keyed by section name alongside section statuses. The data object is round-trip compatible with PATCH /{applicationId}/data. Sections that do not contribute to application progress are excluded. # Save data for multiple sections in a single request Source: https://developers.circle.com/api-reference/circle-mint/onboarding/bulk-save openapi/partner-openapi.yaml patch /v1/onboarding/partner/applications/{applicationId}/data Accepts a JSON object keyed by section name. Each value is the same payload shape as PUT /sections/{sectionName}. Validation is atomic: all sections are validated before any data is saved, and if any section fails validation the entire request is rejected. Saves are sequential: after validation passes, sections are persisted one at a time. Array sections use upsert semantics: items with refId update existing entities, items without refId create new entities. Existing entities not included are preserved. The response data field is an object keyed by section name containing the saved data for each section. # Cancel an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/cancel-application openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId} Cancels a draft ONBOARDING_APPLICATION. Other operation types are not currently cancellable. # Add a comment to an RFI Source: https://developers.circle.com/api-reference/circle-mint/onboarding/create-comment openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments # Delete a comment on an RFI Source: https://developers.circle.com/api-reference/circle-mint/onboarding/delete-comment openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments/{commentId} # Delete a document Source: https://developers.circle.com/api-reference/circle-mint/onboarding/delete-document openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/documents/{documentId} # Download a document Source: https://developers.circle.com/api-reference/circle-mint/onboarding/download-document openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/documents/{documentId} # Retrieve an application by ID Source: https://developers.circle.com/api-reference/circle-mint/onboarding/get-application openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId} # Get the JSON Schema (draft 2020-12) for an application's template Source: https://developers.circle.com/api-reference/circle-mint/onboarding/get-application-schema openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/schema # List active certifications for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/get-certifications openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/certifications Returns the active certifications the end user must agree to for this application, including the HTML content, certification id, and metadata. Pass the returned ids back on POST /{applicationId}/submit as certificationIds to explicitly acknowledge them, or omit certificationIds to let the service auto-resolve the active set. # Get RFI detail with comment history Source: https://developers.circle.com/api-reference/circle-mint/onboarding/get-rfi openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId} # Retrieve section data for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/get-section openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName} Returns field data for the specified section. When typedValues=true, array/object/number field values are emitted as native JSON nodes instead of strings. # List applications for the authenticated partner Source: https://developers.circle.com/api-reference/circle-mint/onboarding/list-applications openapi/partner-openapi.yaml get /v1/onboarding/partner/applications # List documents for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/list-documents openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/documents # List RFI bundles for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/list-rfis openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/rfis # List sections for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/list-sections openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/sections Returns visible sections with statuses. When includeData=true, each entry also includes the section's current field data — useful for rendering a progress view alongside prefilled form data. When typedValues=true (only meaningful with includeData=true), array/object/number field values are emitted as native JSON nodes instead of strings. # Remove an entity from an array section Source: https://developers.circle.com/api-reference/circle-mint/onboarding/remove-entity openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}/{refId} # Save section data for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/save-section openapi/partner-openapi.yaml put /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName} Saves field data for a section. The request body is a JSON object whose shape is defined by the section's JSON Schema (retrieve it via GET /{applicationId}/schema). For MULTIPLE array sections, maxItems is enforced and over-limit payloads return 422. # Submit an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/submit-application openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/submit # Update a comment on an RFI Source: https://developers.circle.com/api-reference/circle-mint/onboarding/update-comment openapi/partner-openapi.yaml put /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments/{commentId} # Submit field data in response to an UPDATE_FIELD or NEW_FIELD RFI Source: https://developers.circle.com/api-reference/circle-mint/onboarding/update-rfidata openapi/partner-openapi.yaml patch /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId} # Upload a document for an application Source: https://developers.circle.com/api-reference/circle-mint/onboarding/upload-document openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/documents # 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`. `purposeOfTransfer` is conditionally required: it must be supplied when the entity's legal entity is Circle SG (`CIRCLE_SG`), or Circle Inc (`CIRCLE_INC`) with Managed Payments enabled; it is optional for other legal entities. `PMT006` is not allowed. Eligible Circle SG (`CIRCLE_SG`) merchants can link a transient payment intent to a Travel Rule customer wallet by supplying `customerWalletId`. # 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. For Singapore (CIRCLE_SG) customers creating a self-hosted (`ownership.custody.type` = `self_hosted`) entry, `metadata.email` is required and the beneficiary PII object matching `identity.type` (`identity.individual` or `identity.business`) is required. The two PII objects are mutually exclusive. For non-regulated jurisdictions the PII objects are accepted if provided but not required. Validation failures can return error codes in the `2024`–`2038` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing, `2038` when `metadata.email` is missing for a self-hosted entry), as well as the PII-missing errors `ADDRESS_BOOK_INDIVIDUAL_PII_MISSING` and `ADDRESS_BOOK_BUSINESS_PII_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. # Postman collection Source: https://developers.circle.com/api-reference/circle-mint/postman Use Circle's Postman collection to send API requests and explore the Circle Mint APIs. Circle's Postman collection provides sample requests for the Circle Mint APIs. Run them in [Postman](https://www.postman.com/), an API client. The collection matches the layout of the [API reference](/api-reference/circle-mint/general/ping). ## Run in Postman Select **Run in Postman** below. Choose one of the following options: * **Fork**: Copies the collection and keeps a link to the parent. * **View**: Lets you try the API without importing it. * **Import**: Copies the collection without keeping a link to Circle's copy. | Collection | Link | | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | API Overview | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82%26entityType%3Dcollection%26workspaceId%3D791ff53c-d236-499c-89ea-307d24ddd289) | | Core Functionality | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f%26entityType%3Dcollection%26workspaceId%3D791ff53c-d236-499c-89ea-307d24ddd289) | | Crypto Deposits API | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325%26entityType%3Dcollection%26workspaceId%3D791ff53c-d236-499c-89ea-307d24ddd289) | | Crypto Payouts API | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/21445022-53206224-cf33-4bbd-8e8a-09d5a780795a?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-53206224-cf33-4bbd-8e8a-09d5a780795a%26entityType%3Dcollection%26workspaceId%3D791ff53c-d236-499c-89ea-307d24ddd289) | ## Authorization To authorize your session, 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/) guide for details. For more information about creating an API key, see [API keys](/api-reference/keys). # 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 # Contracts API Source: https://developers.circle.com/api-reference/contracts Deploy and interact with smart contracts using Circle's managed infrastructure, including event monitoring and contract templates. Smart Contract Platform lets you ship onchain logic without writing deploy scripts, running your own indexer, or building tooling around contract calls. ## Get started Authenticate your requests with an API key. Try the Contracts API with Circle's Postman collection. Set up webhook subscriptions for contract events. ## Endpoint categories Deploy ERC-20, ERC-721, and ERC-1155 contracts from prebuilt templates. Deploy custom bytecode or import existing contracts. List, retrieve, and update managed contracts. Query contract state and submit transactions. Subscribe to onchain events and read event logs. ## OpenAPI specifications The Contracts API reference is generated from these OpenAPI specifications: * `https://developers.circle.com/openapi/configurations_2.yaml` * `https://developers.circle.com/openapi/smart-contract-platform.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # Challenge notification Source: https://developers.circle.com/api-reference/contracts/common/challenges-initialize openapi/configurations_2.yaml webhook challengesInitialize Sent when a user-controlled wallet challenge changes state. The lifecycle state of the challenge is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `COMPLETE`: the end user successfully passed the challenge. The associated operation is initiated automatically. - `FAILED`: the challenge failed (for example, an incorrect PIN or an expired challenge). The operation must be re-initiated. # 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 value is used as the `ID` 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. # Inbound transaction notification Source: https://developers.circle.com/api-reference/contracts/common/transactions-inbound openapi/configurations_2.yaml webhook transactionsInbound Sent when an inbound transaction changes state. The lifecycle state of the transaction is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `CONFIRMED`: the transaction has been broadcast onchain and is awaiting the required number of confirmations. - `COMPLETE`: the transaction has reached the required confirmations and the funds are available in the destination account. # Outbound transaction notification Source: https://developers.circle.com/api-reference/contracts/common/transactions-outbound openapi/configurations_2.yaml webhook transactionsOutbound Sent when an outbound transaction changes state. The lifecycle state of the transaction is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `QUEUED`: the transaction has been initiated but has not yet been processed. - `SENT`: the transaction has been processed and sent to a blockchain node but has not yet been broadcast onchain. - `CONFIRMED`: the transaction has been broadcast onchain and is awaiting the required number of confirmations. - `COMPLETE`: the transaction has reached the required confirmations and the funds are available in the destination account. - `CANCELED`: a cancel request for the transaction has been confirmed. - `FAILED`: the transaction failed (for example, due to insufficient balance or a failed challenge). # 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. # Contracts API error codes Source: https://developers.circle.com/api-reference/contracts/error-codes Descriptions of error codes returned by the Contracts API. For error response shapes and general errors that can be returned by any Circle API, see [API errors](/api-reference/errors). ## 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. | ## Shared contract 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/api-reference/contracts/postman Use Circle's Postman collection to send API requests and explore the Smart Contract Platform APIs. Circle's Postman collection provides sample requests for the Smart Contract Platform APIs. Run them in [Postman](https://www.postman.com/), an API client. The collection matches the layout of the [API reference](/api-reference/contracts/smart-contract-platform/list-contracts). ## Run in Postman Select **Run in Postman** below. Choose one of the following options: * **Fork**: Copies the collection and keeps a link to the parent. * **View**: Lets you try the API without importing it. * **Import**: Copies the collection without keeping 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 To authorize your session, 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/) guide for details. For more information about creating an API key, see [API keys](/api-reference/keys). # 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. # Circle Payments Network API Source: https://developers.circle.com/api-reference/cpn Route and settle stablecoin payments across Circle's network with support for quotes, payments, transactions, and managed payments. Use CPN if you're a bank, fintech, or crypto platform connecting to a shared onchain settlement rail. Pick the integration pattern that fits your stack: direct API access where you operate your own wallets, or a fully Circle-run flow where Circle handles the underlying subaccounts for you. ## Get started Authenticate your requests with an API key. Try the CPN API with Circle's Postman collection. Set up webhook subscriptions for CPN events. ## Endpoint categories Operate your own wallets and settlement: configurations, quotes, payments, and transactions. Use Circle-managed subaccounts for stablecoin payins and payouts. ## OpenAPI specifications The Circle Payments Network API reference is generated from these OpenAPI specifications: * `https://developers.circle.com/openapi/configurations.yaml` * `https://developers.circle.com/openapi/cpn-ofi.yaml` * `https://developers.circle.com/openapi/accounts.yaml` * `https://developers.circle.com/openapi/payments.yaml` * `https://developers.circle.com/openapi/payouts.yaml` * `https://developers.circle.com/openapi/managed-payments.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # 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 # Retrieve all ofi-enabled payment routes Source: https://developers.circle.com/api-reference/cpn/cpn-platform/list-ofi-enabled-routes openapi/cpn-ofi.yaml get /v1/cpn/configurations/enabledRoutes Retrieve all ofi-enabled payment routes. # 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. # Error codes Source: https://developers.circle.com/api-reference/cpn/error-codes Descriptions of error codes returned by the CPN API. When an error is encountered, CPN returns an error code and a message. The sections below show an explanation of each error code returned by the API, and steps to resolve (where possible). For error response shapes and general errors that can be returned by any Circle API, see [API errors](/api-reference/errors). ## Common | Status code | Error code | Detail | Description | | ----------- | ---------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `500` | `-1` | `UNKNOWN_ERROR` | A generic response for errors that aren't properly handled. | | `400` | `2` | `API_PARAMETER_INVALID` | API request was missing parameters or had incorrect parameter types. | | `403` | `3` | `FORBIDDEN` | The client is recognized but does not have permission to access the resource.

**Resolution:** Verify the IP allowlist for the API key has the correct permissions through your Circle representative. | | `401` | `4` | `UNAUTHORIZED` | The client did not provide valid authentication credentials to access the resource.

**Resolution:** Verify your API key was passed as a `Bearer` token in the `Authorization` header of the API request. | | `404` | `2` | `RESOURCE_NOT_FOUND` | The requested resource could not be found. | | `400` | `2900000` | `INVALID_TENANCY_ENV` | The parameter and the environment of the API call doesn't match (for example, a testnet chain in the production environment).

**Resolution:** Update the request body to use the correct parameters. | ## Quote | Status code | Error code | Detail | Description | | ----------- | ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | `290100` | `AMOUNT_OUTSIDE_LIMIT` | The provided amount does not fall in the supported range. It either exceeds the `cryptoLimit` or `fiatLimit`.

**Resolution:** Make a request to the [configuration endpoint](/api-reference/cpn/cpn-platform/get-payment-configurations-overview) to determine supported combinations and try your request again with different parameters. | | `400` | `290101` | `BFI_NOT_AVAILABLE` | Only one BFI services the requested route and that service is unavailable.

**Resolution:** Retry your request at a later time. | | `400` | `290102` | `ROUTE_NOT_SUPPORTED` | The route includes unsupported countries.

**Resolution:** Make a request to the [configuration endpoint](/api-reference/cpn/cpn-platform/get-payment-configurations-overview) to determine supported combinations and try your request again with different parameters. | ## Payment | Status code | Error code | Detail | Description | | ----------- | ---------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `290200` | `QUOTE_NOT_FOUND` | The provided quote ID can't be found.

**Resolution:** Verify the quote ID is correct and make a new API request. | | `400` | `290201` | `QUOTE_ALREADY_IN_USE` | The provided quote ID has been used to create another payment.

**Resolution:** Request a new quote and use it to create your payment. | | `400` | `290202` | `QUOTE_EXPIRED` | The quote used to request payment creation is expired.

**Resolution:** Request a new quote and use it to create your payment. | | `400` | `290203` | `INVALID_SENDER_ADDRESS_BLOCKCHAIN` | The blockchain associated with `senderAddress` does not match the requested blockchain from the quote.

**Resolution:** Use the same blockchain in the payment request body as the `senderAddress`. | | `400` | `290204` | `SANCTIONED_SENDER_WALLET_ADDRESS` | The provided wallet address is on sanctioned lists. | | `400` | `290205` | `PENDING_RFI_VERIFICATION` | An RFI was requested for the sender or recipient so the payment can't be created.

**Resolution:** Complete the RFI for the requested information before creating another payment. | | `400` | `290206` | `RFI_REJECTED` | The given sender or receiver has a rejected RFI with the given BFI and the payment can't be completed. | | `400` | `290207` | `REQUIRED_PARAMETER_MISSING` | A required parameter is missing in the request data.

**Resolution:** Check the error in the response and make a new request with the missing parameter. | | `400` | `290208` | `COMPLIANCE_INFORMATION_REJECTED` | The compliance information provided was rejected. | | `400` | `290211` | `PAYMENT_USE_CASE_MISMATCH` | The `useCase` in the payment request doesn't match the use case the quote derives from its `senderType` and `recipientType`.

**Resolution:** Send the `useCase` the quote implies. `BUSINESS` and `BUSINESS` is `B2B`, `BUSINESS` and `INDIVIDUAL` is `B2C`, `INDIVIDUAL` and `BUSINESS` is `C2B`, and `INDIVIDUAL` and `INDIVIDUAL` is `C2C`. | ## Transaction | Status code | Error code | Detail | Description | | ----------- | ---------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `290300` | `ACTIVE_TRANSACTION_ALREADY_EXISTS` | The payment already has an associated active or complete transaction.

**Resolution:** For payments with an existing active transaction, sign and submit the transaction instead of attempting to create a new one. | | `400` | `290301` | `BLOCKCHAIN_UNSUPPORTED` | The sender address blockchain is not supported by CPN for creating transactions.

**Resolution:** Use the blockchain indicated in the quote. | | `400` | `290302` | `BLOCKCHAIN_UNSUPPORTED_FOR_V2` | The blockchain is not support for Transactions V2.

**Resolution:** See [supported blockchains](/cpn/references/blockchains/supported-blockchains) for the complete list of supported blockchains. | | `400` | `290303` | `INVALID_TRANSACTION_STATUS_FOR_SUBMISSION` | The transaction provided is not in a submittable status.

**Resolution:** Only submit a transaction when it is in the `CREATED` state. | | `400` | `290304` | `INVALID_PAYMENT_STATUS` | The transaction can't be submitted due to an invalid payment state.

**Resolution:** Only submit a transaction when the payment is in the `CRYPTO_FUNDS_PENDING` state. | | `400` | `290305` | `PAYMENT_NOT_FOUND` | The payment ID provided in the request was not found. | | `400` | `290306` | `TRANSACTION_NOT_FOUND` | The transaction provided by the resource ID in the path was not found. | | `400` | `290307` | `SIGNED_TRANSACTION_SUBMITTED` | A signed transaction for the payment has already been submitted.

**Resolution:** Wait for the transaction to reach a terminal state. | | `400` | `290308` | `PAYMENT_EXPIRED` | The payment is expired.

**Resolution:** Request a new quote, then create a new payment and submit the transaction in the appropriate time frame. | | `400` | `290309` | `SIGNED_TRANSACTION_EXPIRED` | The signed transaction is expired. Solana requires that you submit signed transactions in 150 blocks (\~1 min) of signing them.

**Resolution:** Create a new transaction for the same payment and sign and submit it in the appropriate time frame. | | `400` | `290310` | `NONCE_TOO_LOW` | The nonce for the signed transaction is lower than the current wallet nonce.

**Resolution:** Resubmit the signed transaction with the current wallet nonce. | | `400` | `290311` | `SIGNED_TRANSACTION_PAYLOAD_MISMATCH` | The signed transaction payload does not match the CPN payment. For EVM chains, the EIP-3009 typed data must match the value in the `messageToBeSigned` field returned from the [create transaction endpoint](/api-reference/cpn/cpn-platform/create-transaction). For Solana, the signed transaction object must match the value from the `messageToBeSigned` field.

**Resolution:** Resubmit the transaction with the correct payload. | | `400` | `290312` | `INSUFFICIENT_TOKEN_BALANCE` | The sender's wallet does not have enough USDC to complete the transfer.

**Resolution:** Add USDC to the wallet and resubmit the signed transaction. | | `400` | `290313` | `INSUFFICIENT_GAS_BALANCE` | The sender's wallet does not have enough native tokens to cover the gas cost of the transaction.

**Resolution:** Add native tokens to the wallet and resubmit the signed transaction. | | `400` | `290314` | `PAYMENT_ID_MISMATCH` | The payment ID provided in the signed data does not match the expected value. | | `400` | `290315` | `GAS_PRICE_TOO_LOW` | The gas price in the signed transaction is below network thresholds. The fee must exceed the estimated high fee to ensure prompt confirmation.

**Resolution:** Sign the transaction with a higher gas price and resubmit. | | `400` | `290316` | `NONCE_MISMATCH` | When resubmitting the transaction, the wallet address or nonce does not match the original submission.

Sign the transaction with the same wallet and nonce as the previous transaction and resubmit. | | `500` | `290317` | `FULL_NODE_SERVICE_UNAVAILABLE` | The full node used by CPN is unavailable or returning an unexpected error during transaction validation.

**Resolution:** Submit the signed transaction at a later time. | | `400` | `290318` | `PAYMENT_REF_ID_ONCHAIN` | The payment ref ID has already been used onchain. The signed transaction may have been rebroadcast prior to submission to CPN.

**Resolution:** Contact Circle customer support to reconcile the transaction. | | `400` | `290319` | `OUT_OF_GAS` | For EVM chains, the gas limit in the signed transaction is insufficient to cover the execution costs. For Solana, the allocated compute budget falls short of the transaction's requirements, preventing execution.

**Resolution:** For EVM chains, increase the gas limit. For Solana, increase the compute budget. Create a new transaction for the same payment and sign and submit it. | | `400` | `290320` | `NONCE_ALREADY_USED` | The nonce has already been used by the same sender in another signed transaction submission.

**Resolution:** Resubmit the signed transaction using the next available nonce. | | `400` | `290321` | `INVALID_TRANSACTION_SIGNATURE` | The transaction signature is invalid.

**Resolution:** Review the guidelines for signing a transaction and try again with updated signing functions. | | `400` | `290322` | `SANCTIONED_WALLET_ADDRESS` | The wallet address used is on a sanction list. | | `400` | `290324` | `CROSS_CHAIN_UNSUPPORTED` | The crosschain transfer is not supported by CPN.

**Resolution:** Make a request to the [configuration endpoint](/api-reference/cpn/cpn-platform/get-payment-configurations-overview) to determine supported combinations and try your request again with different parameters. | | `400` | `290325` | `COMPLETED_TRANSACTION_EXISTS` | A completed transaction already exists. No need to accelerate. | | `400` | `290327` | `INVALID_SIGNED_TX` | The signed transaction can't be decoded or is otherwise invalid.

**Resolution:** Review the guidelines for signing a transaction and try again with updated signing functions. | | `400` | `290328` | `ONCHAIN_ACCOUNT_NOT_FOUND` | The Solana account specified in the transaction is not found.

**Resolution:** Ensure the Solana account has been initialized before using it as the sender address for the transaction. | | `400` | `290329` | `TRANSACTION_EXPIRED` | The transaction is expired.

**Resolution:** Create a new transaction and submit it in the appropriate time frame. | | `400` | `290330` | `CREATED_PENDING_TRANSACTION_EXIST_AT_ACCELERATION` | The existing transaction is not signed or submitted yet before the current attempt to accelerate it.

**Resolution:** Sign the existing transaction or wait for the signed transaction to be broadcast. | | `400` | `290331` | `NO_TRANSACTION_TO_ACCELERATE` | No broadcast transaction to be accelerated. | | `400` | `290332` | `ONCHAIN_ACCOUNT_INVALID` | An invalid Solana address was used to create the transaction.

**Resolution:** Verify the sender account provided in the request is a valid Solana address capable of signing and sending transactions. Don't use system accounts or program accounts. Resubmit the request using a valid Solana account. | | `400` | `290333` | `BROADCASTING_IN_PROGRESS` | A submitted transaction is already in a non-terminal state.

**Resolution:** Wait for the result. | | `400` | `290334` | `NONCE_TOO_HIGH` | The transaction nonce exceeds the permitted range relative to the current nonce.

**Resolution:** Check your wallets latest nonce and make sure the nonce selected for your transaction does not exceed that value by more than 32. If you are sending multiple concurrent transactions for multiple payments, try waiting for the previous few transactions to settle. | | `400` | `290336` | `INCOMPATIBLE_QUOTE` | The quote is incompatible with the specified transaction. The quote's `transactionVersion` must match the transaction flow you use.

**Resolution:** Make sure the `transactionVersion` specified in the [endpoint response](/api-reference/cpn/cpn-platform/create-quotes) is consistent with the transaction flow you are using. | | `400` | `290337` | `PAYMENT_MISSING_BLOCKCHAIN_ADDRESS` | The payment is missing the sender blockchain address.

**Resolution:** Make sure you [create the payment](/api-reference/cpn/cpn-platform/create-payment) with `blockchain` and `senderAddress`. | | `400` | `290340` | `INSUFFICIENT_ALLOWANCE_TO_PERMIT2` | The ERC-20 allowance granted to the `Permit2` contract is insufficient to cover the total required token amount for the payment, including both the payment amount and associated fees.

**Resolution:** With your sender wallet, approve the `Permit2` contract to spend the required amount of USDC for the payment. | | `400` | `290341` | `PERMIT2_NONCE_ALREADY_USED` | The `Permit2` nonce included in the typed data for the sender has already been used.

**Resolution:** Create a new transaction for the same payment with the same sender address, then sign and submit it. | | `400` | `290352` | `ONCHAIN_SETTLEMENT_CUTOFF_TIME_EXCEEDED` | The signed transaction was submitted after the onchain settlement cutoff. The cutoff time is the payment's expiry time minus a blockchain-specific buffer (for example, \~19 minutes for Ethereum, \~8 minutes for Polygon PoS, \~25 seconds for Solana, and \~0.5 seconds for Arc).

**Resolution:** Request a new quote, create a new payment, and submit the signed transaction before the cutoff time. | ## RFI | Status code | Error code | Detail | Description | | ----------- | ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `290400` | `RFI_EXPIRED` | The RFI has expired; you can't submit data to an expired resource.

**Resolution:** Create a new payment for the user and complete a new RFI. | | `400` | `290401` | `RFI_NOT_SUBMITTABLE` | Requirement data submissions are limited to RFIs with the `INFORMATION_REQUIRED` status. Submission for an RFI in any other status results in this error code. | | `400` | `290402` | `RFI_FILE_NOT_FOUND` | The uploaded filename does not match the file input name specified in the request for information.

**Resolution:** Ensure the correct files are uploaded and that the file names exactly match those specified in the RFI. | | `400` | `290403` | `RFI_FILE_INVALID_CONTENT` | The server was unable to read the content of the uploaded file.

**Resolution:** Ensure the uploaded file contains content in a compatible format that can be processed by the CPN server. | | `400` | `290404` | `RFI_FILE_CONTENT_TOO_LARGE` | The size of the uploaded file exceeds the maximum allowed size.

**Resolution:** Ensure that the uploaded file does not exceed the 10 MB. | | `400` | `290405` | `RFI_UNSUPPORTED_FILE_TYPE` | The MIME type of the uploaded file is not supported.

**Resolution:** Ensure that the uploaded file has a MIME type accepted by the file upload endpoint. | ## Encryption | Status code | Error code | Detail | Description | | ----------- | ---------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `290500` | `ENCRYPTED_BLOB_DECRYPTION_ERROR` | The decryption process failed.

**Resolution:** Ensure the correct public key was used to encrypt the payload. | | `400` | `290502` | `INVALID_JWE_FORMAT` | The JWE compact payload is not formatted correctly or is missing required components.

**Resolution:** Ensure you're using a standard library for encrypting the payload and sending in the JWE compact format. | | `400` | `290503` | `UNSUPPORTED_ENCRYPTION_ALGORITHM` | The JWE compact payload was not encrypted with a supported algorithm.

**Resolution:** Ensure your encryption function is correctly implementing the appropriate algorithms and resubmit. | ## Support ticket | Status code | Error code | Detail | Description | | ----------- | ---------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `400` | `290600` | `TICKET_REFERENCE_REQUIRED` | Ticket reference ID is required for escalation. | | `404` | `290601` | `ORIGINAL_TICKET_NOT_FOUND` | The original ticket was not found with the provided reference ID. | | `400` | `290602` | `INVALID_SUPPORT_TICKET_ISSUE_TYPE` | Issue type is not allowed for this origin.

**Resolution:** Ensure the correct issue type is provided. | | `404` | `290603` | `PAYMENT_NOT_FOUND_FOR_TICKET` | The payment ID provided was not found. | | `500` | `290604` | `SUPPORT_TICKET_SALESFORCE_CREATION_FAILED` | Failed to create a ticket in Circle Salesforce.

**Resolution:** Try again at a later time. | | `500` | `290605` | `SUPPORT_TICKET_BFI_CREATION_FAILED` | Failed to create a BFI support ticket.

**Resolution:** Try again at a later time. | # Archive an account Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/archive-account openapi/accounts.yaml post /v1/accounts/{accountId}/archive Archives a subledger account. Only subledger-type accounts (those created via `POST /v1/accounts`) may be archived, and the account must have a zero balance. While archived, mutating operations on the account are blocked: transfers into or out of the account, new deposit-address generation, fiat withdrawals, payment-intent creation, and fetching wire instructions are all rejected. Read endpoints (e.g. `GET /v1/accounts/{accountId}`, `GET /v1/accounts/deposits`) continue to work. An archived account is excluded from the default `GET /v1/accounts` listing; pass `status=archived` (or include `archived` in a multi-value `status` filter) to retrieve it. Archiving is idempotent: archiving an already-archived account succeeds. An incoming deposit to a pre-existing deposit address or wire instruction on the archived account will auto-unarchive it (status returns to `active`). # 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 by 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. Both flows require an explicit `type` and `purpose` in the request body: - Standard (digital asset / Mint) accounts use `purpose` `custody` and `type` `first_party` or `third_party`. When `clientEntityId` is omitted the account is owned by the caller and `type` must be `first_party`; when `clientEntityId` is provided the account is owned by a sub-entity and `type` must be `third_party`. - Managed Payments intermediary accounts use `type` `first_party` and `purpose` `payments`. # 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. For Singapore (CIRCLE_SG) customers creating a self-hosted (`ownership.custody.type` = `self_hosted`) entry, `metadata.email` is required and the beneficiary PII object matching `identity.type` (`identity.individual` or `identity.business`) is required. The two PII objects are mutually exclusive. For non-regulated jurisdictions the PII objects are accepted if provided but not required. Validation failures can return error codes in the `2024`–`2038` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing, `2038` when `metadata.email` is missing for a self-hosted entry), as well as the PII-missing errors `ADDRESS_BOOK_INDIVIDUAL_PII_MISSING` and `ADDRESS_BOOK_BUSINESS_PII_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`. `purposeOfTransfer` is conditionally required: it must be supplied when the entity's legal entity is Circle SG (`CIRCLE_SG`), or Circle Inc (`CIRCLE_INC`) with Managed Payments enabled; it is optional for other legal entities. `PMT006` is not allowed. Eligible Circle SG (`CIRCLE_SG`) merchants can link a transient payment intent to a Travel Rule customer wallet by supplying `customerWalletId`. # 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. # Postman collection Source: https://developers.circle.com/api-reference/cpn/postman Use Circle's Postman collection to send API requests and explore the Circle Payments Network APIs. Circle's Postman collection provides sample requests for the Circle Payments Network APIs. Run them in [Postman](https://www.postman.com/), an API client. The collection matches the layout of the [API reference](/api-reference/cpn/cpn-platform/get-payment-configurations-overview). ## Run in Postman Select **Run in Postman** below. Choose one of the following options: * **Fork**: Copies the collection and keeps a link to the parent. * **View**: Lets you try the API without importing it. * **Import**: Copies the collection without keeping a link to Circle's copy. | Collection | Link | | :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CPN | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/18225688-f1682adb-ab23-4e2d-b21d-a3562d879e5f?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D18225688-f1682adb-ab23-4e2d-b21d-a3562d879e5f%26entityType%3Dcollection%26workspaceId%3D5d277774-3b09-4e2b-91f5-4253c25a48d0) | ## Authorization To authorize your session, 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/) guide for details. For more information about creating an API key, see [API keys](/api-reference/keys). # Digital Asset Accounts API Source: https://developers.circle.com/api-reference/digital-asset-accounts Open and manage stablecoin accounts and transfers for business customers. Use the Digital Asset Accounts API to open accounts for your customers. Fund by wire or crypto, check balances, and track transfers—all through one REST API. Digital Asset Accounts is a permissioned product. Contact your [Circle representative](mailto:sales@circle.com) to enable API access and get sandbox access before calling the API. ## Get started Learn how Digital Asset Accounts works. See which stablecoins and blockchains are supported. Set up customers with Know Your Business (KYB) before opening accounts. ## Endpoint categories Create and retrieve customer accounts and subaccounts. Organize and manage groups of accounts. Move funds between accounts in your program. View all transactions across account types. Link wire bank accounts for fiat funding. Track incoming wire and crypto deposits. Process withdrawals to bank accounts. Set up blockchain addresses to receive crypto. Manage external addresses for crypto sends. Link ACH bank accounts for fiat deposits and withdrawals. # Archive an account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/archive-account openapi/accounts.yaml post /v1/accounts/{accountId}/archive Archives a subledger account. Only subledger-type accounts (those created via `POST /v1/accounts`) may be archived, and the account must have a zero balance. While archived, mutating operations on the account are blocked: transfers into or out of the account, new deposit-address generation, fiat withdrawals, payment-intent creation, and fetching wire instructions are all rejected. Read endpoints (e.g. `GET /v1/accounts/{accountId}`, `GET /v1/accounts/deposits`) continue to work. An archived account is excluded from the default `GET /v1/accounts` listing; pass `status=archived` (or include `archived` in a multi-value `status` filter) to retrieve it. Archiving is idempotent: archiving an already-archived account succeeds. An incoming deposit to a pre-existing deposit address or wire instruction on the archived account will auto-unarchive it (status returns to `active`). # Bulk-assign accounts to a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/assign-accounts-to-group openapi/accounts.yaml post /v1/accounts/groups/{groupId}/assign Assigns up to 100 accounts to the target custody account group in a single atomic transaction. Accounts already bound to another group are reassigned; rebinding an account that already belongs to this group is a no-op write. Every account must belong to the caller's entity. If any account is not found, the entire request fails with 404 and no assignment row is changed. # Create an account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/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 by 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. Both flows require an explicit `type` and `purpose` in the request body: - Standard (digital asset / Mint) accounts use `purpose` `custody` and `type` `first_party` or `third_party`. When `clientEntityId` is omitted the account is owned by the caller and `type` must be `first_party`; when `clientEntityId` is provided the account is owned by a sub-entity and `type` must be `third_party`. - Managed Payments intermediary accounts use `type` `first_party` and `purpose` `payments`. # Create an account deposit address Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-deposit-address openapi/accounts.yaml post /v1/accounts/addresses/deposit Generates a new blockchain address for an account 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 account with the value of the deposit. # Create a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-group openapi/accounts.yaml post /v1/accounts/groups Creates a new custody account group for the calling entity. If `accountIds` is provided, those accounts are assigned to the new group in the same transaction. Accounts must belong to the caller's entity; accounts already assigned to a different group are reassigned. # Create an account transfer Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-transfer openapi/accounts.yaml post /v1/accounts/transfers Create a transfer from an account to a verified blockchain address or another account. # Create an account bank withdrawal Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/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. # Create an ACH bank account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-achaccount openapi/accounts.yaml post /v1/banks/ach Create a bank account for ACH transfers. # Create a recipient address Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-recipient-address openapi/accounts.yaml post /v1/addresses/recipient Stores an external blockchain address. # Create a wire bank account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/create-wire-account openapi/accounts.yaml post /v1/banks/wires Create a bank account for wire transfers. # Delete a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/delete-account-group openapi/accounts.yaml delete /v1/accounts/groups/{groupId} Soft-deletes a custody account group and hard-deletes every member assignment in a single transaction. Member accounts themselves are left untouched. Deletion is rejected for groups already soft-deleted or owned by a different entity. Responds with `200 OK` and an empty body on success. # Delete a recipient address Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/delete-recipient-address openapi/accounts.yaml delete /v1/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 an account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account openapi/accounts.yaml get /v1/accounts/{accountId} Retrieves a single account by `accountId`. # Get an account bank deposit Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-deposit openapi/accounts.yaml get /v1/accounts/deposits/{id} Returns a bank deposit by ID. # Get an account deposit address Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-deposit-address openapi/accounts.yaml get /v1/accounts/addresses/deposit/{id} Retrieves a specific deposit address. # Get a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-group openapi/accounts.yaml get /v1/accounts/groups/{groupId} Retrieves a single custody account group by `groupId`. Returns the metadata view (`id`, `name`, `createDate`, `updateDate`) only — `approximateBalanceInUsd` and `accountsPreview` are intentionally omitted; see the list endpoint for the aggregate view. # Get an account transfer Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-transfer openapi/accounts.yaml get /v1/accounts/transfers/{id} Retrieves a specific transfer. For onchain activity, outbound transfers typically appear with an account source and blockchain destination, while inbound transfers typically appear with a blockchain source and account destination. # Get an account bank withdrawal Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-withdrawal openapi/accounts.yaml get /v1/accounts/withdrawals/{id} Retrieves a specific bank withdrawal. # Get an ACH bank account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-achaccount openapi/accounts.yaml get /v1/banks/ach/{id} Retrieves a specific ACH bank account. # Get ACH instructions Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-achaccount-instructions openapi/accounts.yaml get /v1/banks/ach/{id}/instructions Retrieves ACH deposit instructions for a specific bank account. # Get a recipient address Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-recipient-address openapi/accounts.yaml get /v1/addresses/recipient/{id} Retrieves a specific recipient address. # Get a wire bank account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/get-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/digital-asset-accounts/all/get-wire-account-instructions openapi/accounts.yaml get /v1/banks/wires/{id}/instructions Retrieves wire transfer instructions for a specific bank account. # List all account deposit addresses Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-deposit-addresses openapi/accounts.yaml get /v1/accounts/addresses/deposit Returns a list of deposit addresses for an account. # List all account bank deposits Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/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. # List custody account groups Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-groups openapi/accounts.yaml get /v1/accounts/groups Retrieves the custody account groups owned by the calling entity. Each row includes an aggregate USD-equivalent balance and a short preview of member account names; use the detail endpoint for metadata only. # List all account transactions Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-transactions openapi/accounts.yaml get /v1/accounts/transactions Searches for transactions across account activity, including deposits, withdrawals, transfers and other activity. Results are returned in descending chronological order. If date parameters are omitted, returns the most recent transactions. # List all account transfers Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-transfers openapi/accounts.yaml get /v1/accounts/transfers Searches for transfers. Results include internal account-to-account transfers, onchain deposits, and onchain withdrawals. Use the `source` and `destination` fields on each transfer to determine directionality. Returns up to 50 transfers in descending chronological order or `pageSize`, if provided. If date parameters are omitted, returns the most recent transfers. # List all account bank withdrawals Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-withdrawals openapi/accounts.yaml get /v1/accounts/withdrawals Lists all bank withdrawals for accounts. # List all accounts Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-accounts openapi/accounts.yaml get /v1/accounts Retrieves the accounts available to the calling entity. # List all recipient addresses Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-recipient-addresses openapi/accounts.yaml get /v1/addresses/recipient Returns a list of recipient addresses that have been verified and are eligible for transfers. # List all wire bank accounts Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/list-wire-accounts openapi/accounts.yaml get /v1/banks/wires Retrieves a list of bank accounts for wire transfers. # Bulk-unassign accounts from a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/unassign-accounts-from-group openapi/accounts.yaml post /v1/accounts/groups/{groupId}/unassign Removes up to 100 accounts from the target custody account group. All-or-nothing: every account listed must currently belong to this group, otherwise the call fails with 404 and no assignment row is removed. # Update an account Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/update-account openapi/accounts.yaml put /v1/accounts/{accountId} Updates mutable fields on an account. Only fields present in the request body are changed; omitted fields are left untouched. # Update a custody account group Source: https://developers.circle.com/api-reference/digital-asset-accounts/all/update-account-group openapi/accounts.yaml put /v1/accounts/groups/{groupId} Renames a custody account group. The new name must be unique (case-insensitive) within the calling entity. # End User Onboarding API Source: https://developers.circle.com/api-reference/end-user-onboarding Create and manage onboarding applications for business customers. Create Know Your Business (KYB) applications for your customers. Add data, upload files, and submit for review—all in one REST API. End User Onboarding API access is enabled per account. Contact your [Circle representative](mailto:sales@circle.com) to get sandbox and production access before calling the API. ## Get started Learn how the onboarding flow works. See how applications change state. ## Endpoint categories Create and retrieve the businesses you onboard. Track applications from draft to final review. Upload and manage compliance documents. Respond to compliance team requests and resubmit data. Read and save sections of an application. # Save data for multiple sections in a single request Source: https://developers.circle.com/api-reference/end-user-onboarding/bulk-save openapi/partner-openapi.yaml patch /v1/onboarding/partner/applications/{applicationId}/data Accepts a JSON object keyed by section name. Each value is the same payload shape as PUT /sections/{sectionName}. Validation is atomic: all sections are validated before any data is saved, and if any section fails validation the entire request is rejected. Saves are sequential: after validation passes, sections are persisted one at a time. Array sections use upsert semantics: items with refId update existing entities, items without refId create new entities. Existing entities not included are preserved. The response data field is an object keyed by section name containing the saved data for each section. # Cancel an application Source: https://developers.circle.com/api-reference/end-user-onboarding/cancel-application openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId} Cancels a draft ONBOARDING_APPLICATION. Other operation types are not currently cancellable. # Add a comment to an RFI Source: https://developers.circle.com/api-reference/end-user-onboarding/create-comment openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments # Create a partner client Source: https://developers.circle.com/api-reference/end-user-onboarding/create-partner-client openapi/customer-orchestration.yaml post /v1/partner/clients Creates a new client entity and initializes an onboarding application in a single request. The response contains both the `clientEntityId` and the `applicationId` for the newly created draft application. A unique combination of `clientName` and `country` is required. Submitting a duplicate combination returns a `409 Conflict` error. # Delete a comment on an RFI Source: https://developers.circle.com/api-reference/end-user-onboarding/delete-comment openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments/{commentId} # Delete a document Source: https://developers.circle.com/api-reference/end-user-onboarding/delete-document openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/documents/{documentId} # Download a document Source: https://developers.circle.com/api-reference/end-user-onboarding/download-document openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/documents/{documentId} # Retrieve data for all visible sections Source: https://developers.circle.com/api-reference/end-user-onboarding/get-all-data openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/data Returns all visible section data keyed by section name alongside section statuses. The data object is round-trip compatible with PATCH /{applicationId}/data. Sections that do not contribute to application progress are excluded. # Retrieve an application by ID Source: https://developers.circle.com/api-reference/end-user-onboarding/get-application openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId} # Get the JSON Schema (draft 2020-12) for an application's template Source: https://developers.circle.com/api-reference/end-user-onboarding/get-application-schema openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/schema # List active certifications for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/get-certifications openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/certifications Returns the active certifications the end user must agree to for this application, including the HTML content, certification id, and metadata. Pass the returned ids back on POST /{applicationId}/submit as certificationIds to explicitly acknowledge them, or omit certificationIds to let the service auto-resolve the active set. # Get a partner client Source: https://developers.circle.com/api-reference/end-user-onboarding/get-partner-client openapi/customer-orchestration.yaml get /v1/partner/clients/{clientEntityId} Returns a single client by its `clientEntityId`, scoped to your partner account. Requesting a client that does not exist — or one that belongs to a different partner — returns a `404 Not Found` error. The response always carries the `clientEntityId` and `created` timestamp. The profile fields (`clientName`, `country`, `clientType`, `businessDetails`) echo the details submitted at creation, while `status` and `balances` are enriched by Circle and omitted when not available. # Get RFI detail with comment history Source: https://developers.circle.com/api-reference/end-user-onboarding/get-rfi openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId} # Retrieve section data for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/get-section openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName} Returns field data for the specified section. When typedValues=true, array/object/number field values are emitted as native JSON nodes instead of strings. # List applications for the authenticated partner Source: https://developers.circle.com/api-reference/end-user-onboarding/list-applications openapi/partner-openapi.yaml get /v1/onboarding/partner/applications # List documents for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/list-documents openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/documents # List partner clients Source: https://developers.circle.com/api-reference/end-user-onboarding/list-partner-clients openapi/customer-orchestration.yaml get /v1/partner/clients Retrieves the clients created under your partner account, newest first. Results are cursor-paginated, forward-only, via the `Link` response header. Each item always carries the `clientEntityId` and `created` timestamp. The profile fields (`clientName`, `country`, `clientType`, `businessDetails`) echo the details submitted at creation, while `status` and `balances` are enriched by Circle and omitted when not available. # List RFI bundles for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/list-rfis openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/rfis # List sections for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/list-sections openapi/partner-openapi.yaml get /v1/onboarding/partner/applications/{applicationId}/sections Returns visible sections with statuses. When includeData=true, each entry also includes the section's current field data — useful for rendering a progress view alongside prefilled form data. When typedValues=true (only meaningful with includeData=true), array/object/number field values are emitted as native JSON nodes instead of strings. # Remove an entity from an array section Source: https://developers.circle.com/api-reference/end-user-onboarding/remove-entity openapi/partner-openapi.yaml delete /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}/{refId} # Save section data for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/save-section openapi/partner-openapi.yaml put /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName} Saves field data for a section. The request body is a JSON object whose shape is defined by the section's JSON Schema (retrieve it via GET /{applicationId}/schema). For MULTIPLE array sections, maxItems is enforced and over-limit payloads return 422. # Submit an application Source: https://developers.circle.com/api-reference/end-user-onboarding/submit-application openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/submit # Update a comment on an RFI Source: https://developers.circle.com/api-reference/end-user-onboarding/update-comment openapi/partner-openapi.yaml put /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId}/comments/{commentId} # Submit field data in response to an UPDATE_FIELD or NEW_FIELD RFI Source: https://developers.circle.com/api-reference/end-user-onboarding/update-rfidata openapi/partner-openapi.yaml patch /v1/onboarding/partner/applications/{applicationId}/rfis/{rfiId} # Upload a document for an application Source: https://developers.circle.com/api-reference/end-user-onboarding/upload-document openapi/partner-openapi.yaml post /v1/onboarding/partner/applications/{applicationId}/documents # API errors Source: https://developers.circle.com/api-reference/errors Common error response formats and codes returned by Circle APIs. Circle APIs return an [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) when they encounter an error: * `4xx` errors are client errors. They communicate a mistake in the request and, where possible, suggest a fix. * `5xx` errors are unexpected server-side errors. Product-specific error codes are documented on each product's error codes page. See [Wallets](/api-reference/wallets/error-codes), [Contracts](/api-reference/contracts/error-codes), [Mint](/api-reference/circle-mint/error-codes), and [CPN](/api-reference/cpn/error-codes). ## General error format HTTP status codes do not always provide sufficient information to determine the cause of the error. The response body contains additional Circle-specific error information. For example, if a request contains an invalid parameter, the response includes the following: **Header** ```http theme={null} HTTP/1.1 400 Bad Request Content-Type: application/json ``` **Body** ```json theme={null} { "code": 2, "message": "API parameter invalid" } ``` ## Extended error format For validation failures, the response includes an `errors` array with per-field detail. For example, if a request is missing a required field: **Header** ```http theme={null} HTTP/1.1 400 Bad Request Content-Type: application/json ``` **Body** ```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 The following errors can be returned for any request. | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ----------------------- | ----------------------------------------------------------------------------------------- | | `-1` | `400` | `Something went wrong` | An unknown error occurred. Retry with exponential backoff. | | `3` | `403` | `Forbidden` | The API key does not have access to the requested endpoint. | | `2` | `400` | `API parameter invalid` | The request body or a parameter value is invalid. Read the `message` field for specifics. | # Gateway API Source: https://developers.circle.com/api-reference/gateway Access and manage a unified USDC balance across multiple blockchains with instant transfers in under 500 ms. Gateway is permissionless and built around non-custodial Gateway Wallet contracts on each supported blockchain. Deposit once, then call the API from your app. ## Get started Learn how Circle Gateway works. See which blockchains Gateway supports. Set up permissionless webhook subscriptions. ## Endpoint categories Read balances, request attestations, and inspect transfers. Submit batched transfer authorizations. Settle and verify x402 payments. ## OpenAPI specifications The Gateway API reference is generated from this OpenAPI specification: * `https://developers.circle.com/openapi/gateway.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # 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. # OpenAPI Specifications Source: https://developers.circle.com/api-reference/openapi-specifications Find the machine-readable OpenAPI specification for every Circle API, with direct links to each published spec file. Use these machine-readable OpenAPI specifications to generate client code, drive API tooling, or give an AI agent accurate request and response shapes. Every API in this reference has one. Every spec is hosted at `https://developers.circle.com/openapi/` and served as a raw YAML file. Add the filename to that base path to fetch a spec directly: ```bash theme={null} curl https://developers.circle.com/openapi/cctp.yaml ``` ## Available specifications Specs are grouped by the product they power. A few specs power more than one product, so they appear under each. Where the same filename appears under multiple products, it's the same file—not a product-specific variant. | Product | Specification | | :---------------------- | :------------------------------------------------------------------------------------------------------------- | | Wallets | [`configurations_1.yaml`](https://developers.circle.com/openapi/configurations_1.yaml) | | | [`configurations_2.yaml`](https://developers.circle.com/openapi/configurations_2.yaml) | | | [`developer-controlled-wallets.yaml`](https://developers.circle.com/openapi/developer-controlled-wallets.yaml) | | | [`user-controlled-wallets.yaml`](https://developers.circle.com/openapi/user-controlled-wallets.yaml) | | | [`buidl-wallets.yaml`](https://developers.circle.com/openapi/buidl-wallets.yaml) | | | [`compliance.yaml`](https://developers.circle.com/openapi/compliance.yaml) | | Contracts | [`configurations_2.yaml`](https://developers.circle.com/openapi/configurations_2.yaml) | | | [`smart-contract-platform.yaml`](https://developers.circle.com/openapi/smart-contract-platform.yaml) | | CCTP | [`cctp.yaml`](https://developers.circle.com/openapi/cctp.yaml) | | Gateway | [`gateway.yaml`](https://developers.circle.com/openapi/gateway.yaml) | | Circle Mint | [`account.yaml`](https://developers.circle.com/openapi/account.yaml) | | | [`general.yaml`](https://developers.circle.com/openapi/general.yaml) | | | [`institutional.yaml`](https://developers.circle.com/openapi/institutional.yaml) | | | [`payments.yaml`](https://developers.circle.com/openapi/payments.yaml) | | | [`payouts.yaml`](https://developers.circle.com/openapi/payouts.yaml) | | | [`cross-currency.yaml`](https://developers.circle.com/openapi/cross-currency.yaml) | | | [`reserve-management.yaml`](https://developers.circle.com/openapi/reserve-management.yaml) | | | [`credit.yaml`](https://developers.circle.com/openapi/credit.yaml) | | | [`partner-openapi.yaml`](https://developers.circle.com/openapi/partner-openapi.yaml) | | Circle Payments Network | [`configurations.yaml`](https://developers.circle.com/openapi/configurations.yaml) | | | [`cpn-ofi.yaml`](https://developers.circle.com/openapi/cpn-ofi.yaml) | | | [`accounts.yaml`](https://developers.circle.com/openapi/accounts.yaml) | | | [`payments.yaml`](https://developers.circle.com/openapi/payments.yaml) | | | [`payouts.yaml`](https://developers.circle.com/openapi/payouts.yaml) | | | [`managed-payments.yaml`](https://developers.circle.com/openapi/managed-payments.yaml) | | StableFX | [`stablefx.yaml`](https://developers.circle.com/openapi/stablefx.yaml) | | xReserve | [`xreserve.yaml`](https://developers.circle.com/openapi/xreserve.yaml) | | Digital Asset Accounts | [`accounts.yaml`](https://developers.circle.com/openapi/accounts.yaml) | | End User Onboarding | [`customer-orchestration.yaml`](https://developers.circle.com/openapi/customer-orchestration.yaml) | | | [`partner-openapi.yaml`](https://developers.circle.com/openapi/partner-openapi.yaml) | Each rendered API reference page also exposes its own source spec: append `.md` to any endpoint page URL to see the underlying `openapi` reference in its frontmatter. # StableFX API Source: https://developers.circle.com/api-reference/stablefx Request quotes and execute institutional FX trades between supported currencies with onchain settlement on Arc. Use StableFX if you're a financial institution—a payment service provider, fintech, crypto OTC desk, or prime broker—working in Request-for-Quote (RFQ) workflows. ## Get started Authenticate your requests with an API key. Try the StableFX API with Circle's Postman collection. Set up webhook subscriptions for trade events. ## Endpoint categories Request a quote for an FX trade. Create, list, and retrieve trades. Register and generate trade and funding signatures. Look up fees for a trade. Fund a trade for settlement. ## OpenAPI specifications The StableFX API reference is generated from this OpenAPI specification: * `https://developers.circle.com/openapi/stablefx.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # Cancel a settlement advance reservation Source: https://developers.circle.com/api-reference/stablefx/all/cancel-settlement-advance-reservation openapi/stablefx.yaml post /v1/exchange/stablefx/settlementAdvances/reservations/{reservationId}/cancel Cancels an active settlement advance reservation, releasing the held credit back to the line. Only reservations in `active` status can be canceled. # Contract maker delegate deliver failed Source: https://developers.circle.com/api-reference/stablefx/all/contract-maker-delegate-deliver-failed openapi/stablefx.yaml webhook contractMakerDelegateDeliverFailed The maker's delegated funding transaction failed on-chain. # 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. # Contract taker delegate deliver failed Source: https://developers.circle.com/api-reference/stablefx/all/contract-taker-delegate-deliver-failed openapi/stablefx.yaml webhook contractTakerDelegateDeliverFailed The taker's delegated funding transaction failed on-chain. # 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. When `fundingMode` is `delegate`, the request must include the trader's delegate-funding authorization in `permit2`/`signature` along with the funder's permit in `funderPermit2`/`funderSignature`. The funder delivers the tokens on the trader's behalf; no tokens transfer from the trader. # 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. When `fundingMode` is `delegate`, the response instead contains two typed-data payloads: one for the trader to sign (a zero-amount authorization) and one for the funder to sign (carrying the actual delivery amount). Delegate mode supports both maker and taker, requires `funderAddress` and `recipientAddress`, and accepts exactly one contract trade ID per request. # 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 Permit2 payload that the maker must sign for a 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 settlement advance credit line Source: https://developers.circle.com/api-reference/stablefx/all/get-settlement-advance-credit openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances/credit Returns the maker's settlement-advance credit line including total limit, current usage, available headroom per currency, and the applicable fee schedule (recurring, draw, and reservation fees). # Get settlement advance detail Source: https://developers.circle.com/api-reference/stablefx/all/get-settlement-advance-detail openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances/{advanceId} Returns the full detail of a single settlement advance including its lifecycle status, advance and collateral amounts, fee information, and any repayments applied. # Get a settlement advance repayment Source: https://developers.circle.com/api-reference/stablefx/all/get-settlement-advance-repayment openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances/repayments/{repaymentId} Returns the details of a specific settlement advance repayment. # Get a settlement advance reservation Source: https://developers.circle.com/api-reference/stablefx/all/get-settlement-advance-reservation openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances/reservations/{reservationId} Returns a single settlement advance reservation by ID. # 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. # List settlement advance reservations Source: https://developers.circle.com/api-reference/stablefx/all/list-settlement-advance-reservations openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances/reservations Returns the maker's settlement advance reservations, optionally filtered by status and currency. Supports cursor-based pagination, newest first. # List settlement advances Source: https://developers.circle.com/api-reference/stablefx/all/list-settlement-advances openapi/stablefx.yaml get /v1/exchange/stablefx/settlementAdvances Returns the maker's settlement advances with their lifecycle status, filterable by status and date range. Supports cursor-based pagination. # Get all trades Source: https://developers.circle.com/api-reference/stablefx/all/list-trades openapi/stablefx.yaml get /v1/exchange/stablefx/trades Returns a cursor-paginated list of all trades, newest first by default. Pagination is navigated through the `Link` response header, not the response body. Follow the `next` relation to walk the collection and stop when it is no longer returned. # Get settlement advance Permit2 typed data for signing Source: https://developers.circle.com/api-reference/stablefx/all/presign-settlement-advance openapi/stablefx.yaml post /v1/exchange/stablefx/signatures/settlementAdvances/presign Returns Permit2 typed-data the maker signs to authorize delegate funding of their side of a trade. The witness is a `DelegateFundingAuthorization` with `permitted.amount = 0` -- Permit2 is used purely as an authorization carrier, no tokens transfer from the maker. Stateless: no persistence, no side effects. # 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. # Repay a settlement advance Source: https://developers.circle.com/api-reference/stablefx/all/repay-settlement-advance openapi/stablefx.yaml post /v1/exchange/stablefx/settlementAdvances/repayments Records a repayment against the credit line backing a settlement advance. Any excess would be balance of the Circle Mint account. **Rounding**: for USDC and EURC repayments, the amount should be rounded up to 2 decimal places. Other stablecoins (e.g. MXNB, QCAD, AUDF, ZARU) are not subject to rounding. Idempotent on `idempotencyKey`: replays with the same key and same amount are no-ops. # Request a settlement advance Source: https://developers.circle.com/api-reference/stablefx/all/request-settlement-advance openapi/stablefx.yaml post /v1/exchange/stablefx/settlementAdvances Submit the Permit2 signature generated from the presign endpoint along with the witness payload. Funding runs asynchronously after this call returns. Idempotent on `tradeId`: replays for the same trade either return the in-progress settlement advance or short-circuit when the trade already has a non-failed active settlement advance. Calling `/reserve` first is optional. The request body is the same whether or not a prior reservation exists. **Collateral rounding**: when the repayment is complete and collateral is released, Circle rounds the collateral amount to 2 decimal places. # Reserve settlement advance credit Source: https://developers.circle.com/api-reference/stablefx/all/reserve-settlement-advance openapi/stablefx.yaml post /v1/exchange/stablefx/settlementAdvances/reserve Holds credit so the maker can lock in a fee snapshot before requesting the settlement advance. Reservations expire after a short window (~15 minutes); after expiry the maker must reserve again. Replaying the same `idempotencyKey` returns the existing reservation. Submitting a different `idempotencyKey` while another reservation for the same currency is still active will be rejected — the maker must cancel the active reservation first before creating a new one. There is one active reservation allowed per currency. A reservation is automatically cancelled after a single settlement advance is made against it. Calling `/reserve` is optional; the maker may skip it and go straight to requesting the settlement advance. # 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 delegate funded Source: https://developers.circle.com/api-reference/stablefx/all/trade-maker-delegate-funded openapi/stablefx.yaml webhook tradeMakerDelegateFunded The maker leg has been funded via a delegated funder. Distinct from makerFunded so subscribers can tell direct-funded legs apart from delegate-funded legs. # 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 delegate funded Source: https://developers.circle.com/api-reference/stablefx/all/trade-taker-delegate-funded openapi/stablefx.yaml webhook tradeTakerDelegateFunded The taker leg has been funded via a delegated funder. Distinct from takerFunded so subscribers can tell direct-funded legs apart from delegate-funded legs. # 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. # Postman collection Source: https://developers.circle.com/api-reference/stablefx/postman Use Circle's Postman collection to send API requests and explore the StableFX APIs. Circle's Postman collection provides sample requests for the StableFX APIs. Run them in [Postman](https://www.postman.com/), an API client. The collection matches the layout of the [API reference](/api-reference/stablefx/all/create-quote). ## Run in Postman Select **Run in Postman** below. Choose one of the following options: * **Fork**: Copies the collection and keeps a link to the parent. * **View**: Lets you try the API without importing it. * **Import**: Copies the collection without keeping a link to Circle's copy. | Collection | Link | | :--------- | :------------------------------------------------------------------------------------------------------------------ | | StableFX | [![Run in Postman](https://run.pstmn.io/button.svg)](https://www.postman.com/circle-solutions/stablefx/collection/) | ## Authorization To authorize your session, 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/) guide for details. For more information about creating an API key, see [API keys](/api-reference/keys). # StableFX API rate limits Source: https://developers.circle.com/api-reference/stablefx/rate-limits Per-entity rate limits and throttling behavior for all StableFX API endpoints. Rate limits control how many requests you can make per second to each StableFX endpoint. Limits apply per entity. Your **entity ID** is the unique identifier assigned to your organization when you register with Circle. StableFX counts requests with a **sliding window** algorithm. The window is one second long. If you send more requests than the limit allows in any rolling one-second span, the API returns an HTTP `429 Too Many Requests` response until your rate drops below the limit. If you receive a `429` response, wait briefly and retry. Use exponential delay between retries. Sending requests at the same rate extends the throttle. The following table lists the per-entity rate limits for all StableFX API endpoints. For examples of calling these endpoints, see the [taker quickstart](/stablefx/quickstarts/fx-trade-taker) and [maker quickstart](/stablefx/quickstarts/fx-trade-maker). To set up the notifications endpoint, see [Set up a webhook endpoint](/api-reference/webhook-endpoints). | Method | Endpoint | Maximum requests per second | | ------ | -------------------------------------- | --------------------------- | | `POST` | `/v1/exchange/stablefx/quotes` | 10 | | `POST` | `/v1/exchange/stablefx/trades` | 10 | | `GET` | `/v1/exchange/stablefx/trades` | 15 | | `GET` | `/v1/exchange/stablefx/signatures/*` | 10 | | `POST` | `/v1/exchange/stablefx/signatures/*` | 10 | | `POST` | `/v1/exchange/stablefx/fund` | 10 | | `GET` | `/v1/exchange/stablefx/fees/{tradeId}` | 15 | | `POST` | `/v1/exchange/stablefx/notifications` | 25 | # How-to: Verify webhook signatures Source: https://developers.circle.com/api-reference/verify-webhook-signatures Confirm that a webhook notification was sent by Circle by verifying its digital signature. Every [v2 webhook notification](/api-reference/webhooks#notification-api-versions) sent by Circle (Circle Wallets, Circle Contracts, CPN payments, Gateway, StableFX) is signed with an asymmetric key. By verifying the signature on each notification, you confirm the payload came from Circle and wasn't tampered with in transit. The verification flow is the same across these products. Only the public key endpoint differs per product. v1 notifications (Circle Mint, Digital Asset Accounts, CPN Managed Payments) use a different signature scheme. Contact your [Circle representative](mailto:sales@circle.com) for details. ## Signature scheme Circle signs each v2 webhook notification with the `ECDSA_SHA_256` algorithm. Every notification includes two headers your endpoint uses to verify the signature: * `X-Circle-Signature`: the digital signature of the notification body, base64-encoded. * `X-Circle-Key-Id`: the UUID of the public key that signed the notification. Each signature is unique to the notification it accompanies, so run the verification flow below for every webhook you receive. ## Verify a signature Extract `X-Circle-Signature` and `X-Circle-Key-Id` from the incoming webhook request's headers. ```text theme={null} X-Circle-Key-Id: 879dc113-5ca4-4ff7-a6b7-54652083fcf8 X-Circle-Signature: MEYCIQCA9EvPbdEJiy7Cw0eY+KQZA/oFi5ZEInPs8CYpyaJexgIhAKtRNnDz9QRQmFKx8QFrvawp+8b9Bs2dQ03xD+XaWVDE ``` Using the value of `X-Circle-Key-Id`, call your product's public key endpoint to retrieve the public key and algorithm. Replace `` with the endpoint for your product: * [Wallets](/api-reference/wallets/common/get-notification-signature), [Contracts](/api-reference/contracts/common/get-notification-signature), and [Gateway](/api-reference/gateway/all/get-permissionless-notification-signature): `/v2/notifications/publicKey/{keyId}` * [CPN](/api-reference/cpn/common/get-notification-signature): `/v2/cpn/notifications/publicKey/{keyId}` * [StableFX](/api-reference/stablefx/all/get-notification-signature): `/v2/stablefx/notifications/publicKey/{id}` ```bash theme={null} curl --request GET \ --url 'https://api.circle.com//879dc113-5ca4-4ff7-a6b7-54652083fcf8' \ --header 'Authorization: Bearer $CIRCLE_API_KEY' ``` A successful response returns the base64-encoded public key: ```json theme={null} { "data": { "id": "879dc113-5ca4-4ff7-a6b7-54652083fcf8", "algorithm": "ECDSA_SHA_256", "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg==", "createDate": "2026-01-15T21:47:35.107250Z" } } ``` The public key for a given `keyId` is static, so cache the result to avoid fetching it on every webhook. Use the public key to verify the signature against the **raw** request body. Parsing the JSON and re-serializing it changes the byte order, so the signature no longer matches. ```typescript Node.js theme={null} import { createVerify, createPublicKey, KeyObject } from "crypto"; // Cache the public key by keyId to avoid refetching on every webhook. const publicKeyCache = new Map(); async function getPublicKey(keyId: string): Promise { const cached = publicKeyCache.get(keyId); if (cached) return cached; // Replace with your product's endpoint. const response = await fetch( `https://api.circle.com//${keyId}`, { headers: { Authorization: `Bearer ${process.env.CIRCLE_API_KEY}` } }, ); const { data } = await response.json(); const publicKey = createPublicKey({ key: Buffer.from(data.publicKey, "base64"), format: "der", type: "spki", }); publicKeyCache.set(keyId, publicKey); return publicKey; } export async function verifyWebhook( rawBody: string, signature: string, keyId: string, ): Promise { const publicKey = await getPublicKey(keyId); const verifier = createVerify("SHA256"); verifier.update(rawBody); return verifier.verify(publicKey, signature, "base64"); } ``` ```python Python theme={null} import base64 import os import requests from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec # Cache the public key by keyId to avoid refetching on every webhook. public_key_cache: dict = {} def get_public_key(key_id: str): if key_id in public_key_cache: return public_key_cache[key_id] # Replace with your product's endpoint. response = requests.get( f"https://api.circle.com//{key_id}", headers={"Authorization": f"Bearer {os.environ['CIRCLE_API_KEY']}"}, ) data = response.json()["data"] public_key_bytes = base64.b64decode(data["publicKey"]) public_key = serialization.load_der_public_key(public_key_bytes) public_key_cache[key_id] = public_key return public_key def verify_webhook(raw_body: bytes, signature_b64: str, key_id: str) -> bool: public_key = get_public_key(key_id) signature_bytes = base64.b64decode(signature_b64) try: public_key.verify( signature_bytes, raw_body, ec.ECDSA(hashes.SHA256()), ) return True except InvalidSignature: return False ``` If verification succeeds, the notification is authentic. If it fails, reject the request. # Wallets API Source: https://developers.circle.com/api-reference/wallets Create and manage developer-controlled and user-controlled wallets, execute transactions, and sign messages across supported blockchains. Programmable Wallets is Circle's embedded MPC wallet platform. Use it when you need secure key management without running your own custody stack. ## Get started Authenticate your requests with an API key. Try the Wallets API with Circle's Postman collection. Set up webhook subscriptions for wallet events. ## Endpoint categories Manage wallet sets and wallets that your backend signs for. Manage wallets where your end users hold the signing key with a PIN. Read transfers, user operations, and balances for Modular Wallets. Manage monitored tokens, developer account keys, and the faucet. Screen addresses against compliance rules. ## OpenAPI specifications The Wallets API reference is generated from these OpenAPI specifications: * `https://developers.circle.com/openapi/configurations_1.yaml` * `https://developers.circle.com/openapi/configurations_2.yaml` * `https://developers.circle.com/openapi/developer-controlled-wallets.yaml` * `https://developers.circle.com/openapi/user-controlled-wallets.yaml` * `https://developers.circle.com/openapi/buidl-wallets.yaml` * `https://developers.circle.com/openapi/compliance.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # 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. # Challenge notification Source: https://developers.circle.com/api-reference/wallets/common/challenges-initialize openapi/configurations_2.yaml webhook challengesInitialize Sent when a user-controlled wallet challenge changes state. The lifecycle state of the challenge is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `COMPLETE`: the end user successfully passed the challenge. The associated operation is initiated automatically. - `FAILED`: the challenge failed (for example, an incorrect PIN or an expired challenge). The operation must be re-initiated. # 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 value is used as the `ID` 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. # Inbound transaction notification Source: https://developers.circle.com/api-reference/wallets/common/transactions-inbound openapi/configurations_2.yaml webhook transactionsInbound Sent when an inbound transaction changes state. The lifecycle state of the transaction is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `CONFIRMED`: the transaction has been broadcast onchain and is awaiting the required number of confirmations. - `COMPLETE`: the transaction has reached the required confirmations and the funds are available in the destination account. # Outbound transaction notification Source: https://developers.circle.com/api-reference/wallets/common/transactions-outbound openapi/configurations_2.yaml webhook transactionsOutbound Sent when an outbound transaction changes state. The lifecycle state of the transaction is conveyed by the `state` field on the `notification` object. States covered by this notification type: - `QUEUED`: the transaction has been initiated but has not yet been processed. - `SENT`: the transaction has been processed and sent to a blockchain node but has not yet been broadcast onchain. - `CONFIRMED`: the transaction has been broadcast onchain and is awaiting the required number of confirmations. - `COMPLETE`: the transaction has reached the required confirmations and the funds are available in the destination account. - `CANCELED`: a cancel request for the transaction has been confirmed. - `FAILED`: the transaction failed (for example, due to insufficient balance or a failed challenge). # 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. 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/." To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` 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 # Wallets API error codes Source: https://developers.circle.com/api-reference/wallets/error-codes Descriptions of error codes returned by the Wallets and Compliance Engine APIs. For error response shapes and general errors that can be returned by any Circle API, see [API errors](/api-reference/errors). For error codes surfaced by the user-controlled wallet SDK's built-in UI (`155701`–`155721`), see [SDK error codes](/sdks/user-controlled/error-codes). ## User (`155101`–`155146`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `155101` | `409` | `Existing user already created with the provided userId.` | Retry with a unique user ID. | | `155102` | `404` | `Cannot find the user id in the system.` | Verify the user ID and confirm your entity has access to it. | | `155103` | `401` | `Cannot find the user token in the system.` | Generate a new user token. | | `155104` | `403` | `The userToken had expired.` | Generate a new user token. | | `155105` | `403` | `The userToken is invalid.` | Generate a new user token. | | `155106` | `409` | `The user had already been initialized.` | Query the user status to check whether the user has already set a PIN and created a wallet. | | `155107` | `409` | `User has previously set a PIN. Use PUT /user/pin to reset the PIN.` | Reset the PIN with `PUT /user/pin`. | | `155108` | `409` | `User has previously set up their security questions, and they can't be reset.` | Security questions can only be set once and cannot be reset. | | `155109` | `409` | `The specified user has been disabled.` | Re-enable the user. | | `155110` | `400` | `User has not set up a PIN yet.` | Initialize the user or set a PIN before retrying. | | `155111` | `400` | `User hasn't set the security questions for PIN backup yet.` | Initialize the user or set security questions before retrying. | | `155112` | `400` | `The user has inputted the incorrect pin.` | Retry with the correct PIN. | | `155113` | `400` | `Provided device ID is not found in the system.` | Verify the device ID. | | `155114` | `400` | `Provided app ID is not recognized in the system.` | Verify the app ID. Find it in the Circle Console or via `GET /config/entity`. | | `155115` | `400` | `The user has inputted the incorrect security answers` | Retry with the correct security answers. | | `155116` | `404` | `The challenge ID doesn't exist in the system.` | Verify the challenge ID. | | `155117` | `400` | `The content provided for approval is not correct.` | Update to the latest SDK version. | | `155118` | `400` | `Encryption key does not match with the user's token. Call POST /users/token to get the correct token encryption key pair.` | Call `POST /users/token` to get a matching token and encryption keypair. | | `155119` | `400` | `The user's PIN input is locked. It will be unlocked after the cooldown period.` | Wait for the cooldown to end, then retry. | | `155120` | `400` | `The user's security questions input is locked. It will be unlocked after the cooldown period.` | Wait for the cooldown to end, then retry. | | `155121` | `403` | `The provided challengeId has expired.` | Create a new challenge. | | `155122` | `403` | `The provided challengeId is invalid` | Create a new challenge. | | `155123` | `403` | `No extra information provided when adding PIN-related requests.` | Update to the latest SDK version. The challenge approval payload is malformed. | | `155124` | `403` | `The extra information provided for PIN-related requests is invalid.` | Update to the latest SDK version. The challenge approval payload is malformed. | | `155130` | `400` | `User OTP token is expired.` | Request a new OTP email. | | `155131` | `400` | `User OTP token is invalid.` | Verify the OTP token in the request. | | `155132` | `404` | `User OTP value is not found.` | Request an OTP email first. | | `155133` | `400` | `User OTP value is invalid.` | Verify the OTP value in the request. | | `155134` | `400` | `User OTP value is not matched.` | Confirm the OTP value matches the OTP token. | | `155135` | `400` | `User's email is invalid.` | Verify the email format. | | `155136` | `400` | `User's email is not matched.` | Confirm the email matches the user's registered email. | | `155137` | `400` | `User's device ID is invalid.` | Verify the device ID in the request. | | `155138` | `400` | `Failed to send the email.` | Check the SMTP configuration in the Circle Console. | | `155139` | `400` | `The idToken / accessToken of SSO is expired.` | Refresh the SSO token from your SSO provider. | | `155140` | `400` | `Failed to validate the idToken/ accessToken.` | Verify the SSO provider configuration in the Circle Console. | | `155141` | `400` | `The user has exceeded the max limit (5) of entering OTP at the moment.` | Wait 60 minutes before retrying. | | `155142` | `400` | `The max limit (5) of sending OTP has been exceeded at the moment.` | Wait 60 minutes before retrying. | | `155143` | `400` | `The device token is expired.` | Request a new device token. Tokens expire after 10 minutes. | | `155144` | `400` | `The device token is invalid.` | Request a new device token. Only one active token is allowed per `deviceId`. | | `155145` | `404` | `The device token is not found.` | Request a new device token. | | `155146` | `400` | `The OTP token is invalid as the user has entered it incorrectly three times.` | Request a new OTP email. | ## Authentication configuration (`155150`–`155157`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ---------------------------------------------- | ------------------------------------------------------ | | `155150` | `404` | `The SMTP server configuration is not found.` | Complete the SMTP configuration in the Circle Console. | | `155151` | `400` | `The SMTP server configuration is invalid.` | Fix the SMTP configuration in the Circle Console. | | `155152` | `404` | `The SSO provider configuration is not found.` | Add an SSO provider in the Circle Console. | | `155154` | `400` | `The OTP email template is invalid.` | Fix the OTP email template configuration. | | `155155` | `404` | `The OTP email template is not found.` | Add an OTP email template in the Circle Console. | | `155156` | `400` | `Failed to update the email template.` | Check the OTP email template content. | | `155157` | `400` | `Failed to update the SMTP server` | Check the SMTP configuration in the Circle Console. | ## Transaction (`155201`–`155264`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `155201` | `400` | `Not enough funds to fulfill the withdraw request.` | Add native tokens to the wallet to cover pending transactions. | | `155202` | `400` | `Transaction nonce is inconsistent with sender's latest nonce.` | Retry the transaction after the previous one reaches finality. | | `155203` | `400` | `User op nonce can not be larger than 0 when smart contract wallet hasn't been deployed.` | Send the first user op with nonce 0 to trigger contract deployment. | | `155204` | `400` | `The total cost of executing transaction is higher than the balance of the user's account when estimating fee.` | Add native tokens to cover the total transaction cost. | | `155205` | `400` | `Failed to execute this request on EVM due to insufficient token when estimating fee.` | Add the required non-native token balance to the wallet. | | `155206` | `400` | `The sender address is not token owner or approved when estimating token transfer.` | Approve the caller to spend the token, or use the token owner as sender. | | `155207` | `400` | `Gas required exceeds allowance when estimating fee.` | Increase the gas allowance. | | `155208` | `400` | `Estimate fee execution reverted.` | The transaction reverted onchain during fee estimation. Verify the target contract, function, and parameters. | | `155209` | `400` | `ABI function signature can't pack ABI parameter.` | Correct the ABI parameters. | | `155210` | `400` | `Fails to perform transaction estimation.` | Retry the request. If the error persists, verify the request parameters against the target blockchain. | | `155211` | `400` | `MaxFee * GasLimit exceed configurable max transaction fee (default is 1 native token).` | Reduce `maxFee` or `gasLimit`. Circle applies a per-blockchain maximum fee limit, which defaults to 1 unit of the blockchain's native token. | | `155215` | `400` | `Unsupported operation for transaction.` | Check the transaction status. The requested operation is not valid in its current state. | | `155218` | `400` | `Invalid number of nft in transaction request.` | Provide multiple tokens only for ERC-1155 transfers. | | `155219` | `400` | `Invalid destination address.` | Provide a valid destination address. | | `155220` | `400` | `Wallet and token's blockchain mismatch.` | Use a wallet and token on the same blockchain. | | `155221` | `400` | `Invalid amounts in transfer request.` | For token standards other than ERC-1155, provide a single amount in the transfer request. | | `155222` | `404` | `NFT metadata can not be found.` | Confirm that the token contract exposes NFT metadata. | | `155223` | `400` | `Unsupported userId for get transactions.` | Remove `userId` from the query parameters. | | `155224` | `400` | `Failed to parse the provided amounts in request to decimals.` | Provide amounts as valid decimal numbers. | | `155225` | `400` | `Wallet and request's blockchain mismatch.` | Use a wallet and request blockchain that match. | | `155226` | `400` | `Invalid source address.` | Provide a valid source address. | | `155227` | `400` | `Invalid transaction type.` | Provide an outbound transaction. | | `155228` | `400` | `Missing token ID.` | Include the token ID in the request. | | `155229` | `400` | `Transaction is not eligible for operation.` | Check the transaction status. The operation is no longer allowed in its current state. | | `155230` | `400` | `No call data or abi signature provided.` | Provide `callData` or an ABI signature. | | `155231` | `400` | `Transaction needs feeLevel or gasLimit provided.` | Provide `feeLevel` or `gasLimit`. | | `155232` | `400` | `SCA transaction needs feeLevel provided.` | Provide `feeLevel` for SCA transactions. | | `155233` | `400` | `Provided gasLimit is too low to complete the requested transaction.` | Provide a higher `gasLimit` than the network estimate. | | `155234` | `400` | `Transaction can't have both feeLevel and fee parameters provided.` | Provide either `feeLevel` or the detailed fee parameters, not both. | | `155235` | `400` | `EIP1559 chains need maxFee/priorityFee provided.` | Provide `maxFee` and `priorityFee` for EIP-1559 blockchains. | | `155236` | `400` | `Failed to parse the provided fee in request to decimals.` | Provide fee values as valid decimal numbers. | | `155237` | `400` | `PriorityFee cannot be larger than maxFee in creating transaction request.` | Set `priorityFee` less than or equal to `maxFee`. | | `155238` | `400` | `Non-EIP1559 chains need gasPrice provided.` | Provide `gasPrice` for non-EIP-1559 blockchains. | | `155239` | `400` | `Invalid token address for transfer.` | Provide a valid token address. | | `155240` | `400` | `Invalid token standard for transfer.` | Provide a supported token standard. | | `155241` | `400` | `Invalid token decimal for transfer.` | Provide a valid token decimal. | | `155242` | `400` | `The lengths of amounts and nft tokens don't match.` | Ensure the amounts and NFT token arrays have the same length. | | `155243` | `400` | `Missing bytecode for contract deployment.` | Provide `bytecode` for the contract deployment. | | `155244` | `400` | `Cannot provide both WalletID and SourceAddress/Blockchain.` | Provide either `walletId` or `sourceAddress` and `blockchain`, not both. | | `155245` | `400` | `Invalid amount in contract execution request.` | Provide the amount in the contract execution request as a valid number. | | `155247` | `400` | `Cannot provide both CallData and AbiFunctionSignature/AbiParameters.` | Provide either `callData` or `abiFunctionSignature` and `abiParameters`, not both. | | `155248` | `400` | `Policy is not activated and cannot be used.` | Activate the [Gas Station policy](/wallets/gas-station/policy-management) before submitting transactions. | | `155249` | `400` | `Exceeded max daily transaction of the policy.` | Wait for the daily counter to reset, or raise the policy's max daily transaction limit. | | `155250` | `400` | `Exceeded max spend USD per transaction of the policy.` | Reduce the transaction amount, or raise the policy's per-transaction USD limit. | | `155251` | `400` | `Exceeded max spend USD daily of the policy.` | Wait for the daily counter to reset, or raise the policy's daily USD spend limit. | | `155252` | `400` | `Exceeded max native token daily of the policy.` | Wait for the daily counter to reset, or raise the policy's daily native token limit. | | `155253` | `400` | `Sender is in policy blocklist.` | Remove the sender from the policy's blocklist, or use a wallet not on the blocklist. | | `155254` | `400` | `Non-EIP1559 chains don't support maxFee/priorityFee.` | Remove `maxFee` and `priorityFee`. Use `gasPrice` for non-EIP-1559 blockchains. | | `155255` | `400` | `The entity is restricted in paymaster.` | Check the account's [Gas Station policy](/wallets/gas-station/policy-management) configuration. | | `155256` | `400` | `Default policy for the blockchain is not found in paymaster.` | Configure a default [Gas Station policy](/wallets/gas-station/policy-management) for the blockchain. | | `155264` | `400` | `Wait for pending transactions to be included on the blockchain before submitting new requests. EVM chains restrict the number of queued transactions per sender address.` | Wait for pending transactions to finalize. To increase throughput, distribute transactions across multiple wallets. See [Transaction limits and optimizations](/wallets/transaction-limits-and-optimizations). | ## Wallet (`155501`–`155515`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `155501` | `409` | `Frozen wallets can not be updated or interact with, only query.` | Frozen wallets support query only. | | `155502` | `403` | `Max amount of wallets (tentative 1M) reached under 1 wallet set.` | Create a new wallet set to add more wallets. | | `155503` | `400` | `Metadata array length needs to match wallet count in create developer wallets request.` | Match the `metadata` array length to the wallet count in `POST /developer/wallets`. | | `155504` | `400` | `Metadata array length needs to match number of blockchains in create user wallets request.` | Match the `metadata` array length to the number of blockchains in the create-user-wallet request. | | `155505` | `400` | `SCA wallet needs to wait for first-time transaction to be queued before processing more transactions.` | Wait for the first SCA transaction to be queued before submitting more. | | `155506` | `400` | `SCA wallet config is invalid.` | Check the SCA wallet configuration. | | `155507` | `400` | `SCA account is not supported on the given blockchain.` | Use a supported blockchain. See [supported blockchains](/wallets/supported-blockchains). | | `155509` | `400` | `Entity needs to setup paymaster policy on Mainnet before SCA account creation. Please check paymaster policy setup` | Configure a single paymaster policy for the blockchain in the Circle Console. | | `155510` | `400` | `The operation is not supported on the blockchain you specify.` | Use a wallet on a blockchain that supports this operation. | | `155511` | `400` | `Blockchain is not supported for wallet creation.` | Use a supported blockchain. See [supported blockchains](/wallets/supported-blockchains). | | `155512` | `404` | `The owner of the SCA wallet can not be found.` | Check whether ownership was transferred or the account is set up correctly. | | `155515` | `400` | `The provided wallet account type is not supported by this API.` | Use a supported account type. See [account types](/wallets/account-types). | ## Wallet set (`155601`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ----------------------------------------------------- | ---------------------------------- | | `155601` | `400` | `Failed to retrieve wallet set which already exists.` | Retry with a unique wallet set ID. | ## Transaction signing (`155801`–`155808`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ------------------------------------------------ | ------------------------------------------------------- | | `155801` | `400` | `Transaction or rawTransaction is invalid.` | Provide a valid `transaction` or `rawTransaction`. | | `155802` | `400` | `Account not found.` | Verify the account in the transaction header. | | `155803` | `400` | `Not signer account.` | Sign from the account that is the transaction's signer. | | `155804` | `400` | `Transaction is already signed.` | No action required. | | `155805` | `400` | `Transaction failed to deserialize.` | Check the transaction format. | | `155806` | `400` | `The transaction size exceeds blockchain limit.` | Reduce the transaction payload. | | `155807` | `400` | `The chain ID is not supported for signing.` | Use a supported chain ID for EVM signing. | | `155808` | `400` | `The chain ID is missing in sign request.` | Include a chain ID in the typed data or transaction. | ## Shared (`156001`–`156030`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | `156001` | `404` | `Cannot find target wallet in the system. Either the specified wallet doesn't exist or it's not accessible to the caller.` | Verify the wallet ID. | | `156002` | `404` | `Cannot find target token in the system. Either the specified token doesn't exist or it's not accessible to the caller.` | Verify the token ID. | | `156003` | `404` | `Cannot find target transaction in the system. Either the specified transaction doesn't exist or it's not accessible to the caller.` | Verify the transaction ID. | | `156004` | `400` | `Reusing an entity secret ciphertext is not allowed. Please re-encrypt the entity secret to generate new ciphertext.` | Re-encrypt the entity secret to generate a new ciphertext. | | `156005` | `404` | `Cannot find target wallet set in the system. Either no such wallet set, or it's not accessible to the caller.` | Verify the wallet set ID. | | `156006` | `400` | `TEST_API key cannot be used with blockchain mainnets, or LIVE_API key cannot be used with blockchain testnets.` | Use a `TEST_API` key for testnets and a `LIVE_API` key for mainnets. | | `156007` | `401` | `TEST_API key or LIVE_API key is not found for the request.` | Provide a valid API key. | | `156008` | `400` | `Cannot find target entity config in the system. Either no such entity config, or it's not accessible to the caller.` | Verify the entity config. | | `156009` | `400` | `Fail to parse id as UUID in url.` | Provide an ID in UUID format. | | `156010` | `404` | `Cannot find the corresponding entity in the system.` | Verify the entity. | | `156011` | `404` | `Cannot find target nftTokenId in the system.` | Verify the `nftTokenId`. | | `156012` | `404` | `Cannot find corresponding pagination cursor in the system.` | Correct the pagination parameters. | | `156013` | `400` | `The provided entity secret is invalid.` | Create or re-encrypt the entity secret. | | `156014` | `400` | `Pagination params are invalid. Only UUID format is supported for pageBefore and pageAfter.` | Use UUID format for `pageBefore` and `pageAfter`. | | `156015` | `409` | `The secret for this entity has already been set.` | No action required. | | `156016` | `403` | `The entity secret has not been set yet. Please provide encrypted ciphertext in the console.` | Provide encrypted ciphertext in the Circle Console. | | `156017` | `400` | `The specified blockchain parameters are incorrect.` | Correct the blockchain parameters. | | `156018` | `403` | `The uploaded recovery file is invalid.` | Upload a valid recovery file. | | `156019` | `403` | `Current entity secret is invalid. Please rotate the entity secret first.` | Rotate the entity secret in the Circle Console. | | `156020` | `403` | `Please use a new idempotency key.` | Use a new idempotency key. | | `156021` | `409` | `A new wallet set ID is required.` | Use a new wallet set ID. | | `156023` | `409` | `EncodedByHex is true in sign request, but the message is not hex encoded.` | Provide a hex-encoded message, or set `encodedByHex` to `false`. | | `156024` | `400` | `Data is not a valid JSON string in sign request.` | Provide a valid JSON typed data string. | | `156025` | `400` | `Invalid message in request.` | Provide a valid EIP-191 message. | | `156026` | `400` | `Invalid typed data in request.` | Provide valid EIP-712 typed data. | | `156027` | `400` | `The specified blockchain is either not supported or deprecated.` | Use a supported blockchain. See [supported blockchains](/wallets/supported-blockchains). | | `156030` | `400` | `Invalid unsigned delegate action in request.` | Provide a valid delegate action. | ## Compliance Engine screening (`280001`–`280002`) | Error code | HTTP code | Error message | Resolution | | ---------- | --------- | ---------------------------- | ----------------------------------- | | `280001` | `400` | `Unsupported blockchain` | Use a supported blockchain. | | `280002` | `400` | `Invalid blockchain address` | Provide a valid blockchain address. | # Postman collection Source: https://developers.circle.com/api-reference/wallets/postman Use Circle's Postman collection to send API requests and explore the Programmable Wallets APIs. Circle's Postman collection provides sample requests for the Programmable Wallets APIs. Run them in [Postman](https://www.postman.com/), an API client. The collection matches the layout of the [API reference](/api-reference/wallets/common/ping). ## Run in Postman Select **Run in Postman** below. Choose one of the following options: * **Fork**: Copies the collection and keeps a link to the parent. * **View**: Lets you try the API without importing it. * **Import**: Copies the collection without keeping a link to Circle's copy. | Collection | Link | | :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wallets | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/15022472-80b87d8a-844b-422e-9c2a-3a6ce86a2644?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D15022472-80b87d8a-844b-422e-9c2a-3a6ce86a2644%26entityType%3Dcollection%26workspaceId%3D73acd722-fab9-49b0-9382-086659476258) | ## Authorization To authorize your session, 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/) guide for details. For more information about creating an API key, see [API keys](/api-reference/keys). ## Entity secret Developer-controlled wallets need an entity secret. Register one first. See the [Register your entity secret](/wallets/dev-controlled/register-entity-secret) quickstart. Add your **hex-encoded entity secret** as a Postman variable. Don't use the encrypted ciphertext. The collection's helper scripts re-encrypt the secret before each call to meet Circle's uniqueness rule. Run the **Get public key for entity** request once. The collection stores the key as a variable and uses it to encrypt your secret. # 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. # Wallets API rate limits Source: https://developers.circle.com/api-reference/wallets/rate-limits Rate limits for the Circle Wallets API Wallets API endpoints are rate limited per second. The defaults are: * **GET**: 20 requests per second * **POST**: 5 requests per second The following endpoints are limited to 10 requests per second: * `POST /v1/w3s/developer/wallets` * `PUT /v1/w3s/developer/wallets/{id}/blockchains/{blockchain}` * `POST /v1/w3s/transactions/transfer/estimateFee` * `POST /v1/w3s/transactions/contractExecution/estimateFee` * `POST /v1/w3s/users/token` * `POST /v1/w3s/user/sign/transaction` * `POST /v1/w3s/user/sign/message` * `POST /v1/w3s/user/sign/typedData` * `POST /v1/w3s/developer/sign/transaction` * `POST /v1/w3s/developer/sign/message` * `POST /v1/w3s/developer/sign/typedData` * `POST /v1/w3s/developer/sign/delegateAction` When a request exceeds the rate limit, the API returns HTTP `429 Too Many Requests`. Cache static values such as token IDs and public keys on the client to avoid unnecessary calls. # 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. # 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. # 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. # 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/. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. # 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. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. 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/. To identify the wallet, you must provide either `walletId`, or both `walletAddress` and `blockchain` in the request body. # 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. # How-to: Set up a webhook endpoint Source: https://developers.circle.com/api-reference/webhook-endpoints Expose a subscriber endpoint and subscribe to webhook notifications from a Circle product. To start receiving webhook notifications from a Circle product, expose a subscriber endpoint, then register that endpoint as a subscriber to the events you care about. The exact flow depends on which [notification API version](/api-reference/webhooks#notification-api-versions) the product uses. Used by [Circle Wallets](/wallets), [Circle Contracts](/contracts), [CPN payments](/cpn), [Gateway](/gateway), and [StableFX](/stablefx). Expose a publicly accessible HTTPS endpoint that: * Is reachable from the public internet. * Handles both `HEAD` and `POST` requests. Circle uses `HEAD` to validate the URL when you create or update a subscription, and `POST` to deliver notifications. * Responds to `POST` requests with a `200 OK` status code so Circle treats the delivery as successful. Any other status causes Circle to retry the notification. To test before deploying a real endpoint, generate a temporary URL with [webhook.site](https://webhook.site/) and use it as your subscriber endpoint. Configure your firewall, load balancer, or cloud security groups so your endpoint only trusts webhook requests from Circle's source IP addresses. This blocks unauthenticated traffic at the network edge as a layer of defense in addition to [signature verification](/api-reference/verify-webhook-signatures). Allowlist the IP addresses for each product you integrate with separately. **Wallets, Contracts, and Gateway** share the same webhook delivery infrastructure: * `54.243.112.156` * `100.24.191.35` * `54.165.52.248` * `54.87.106.46` **Circle Payments Network (CPN)**: the same IP set applies to both mainnet and testnet: * `35.169.154.32` * `3.90.127.28` * `3.230.111.7` * `54.88.227.75` **Stablecoin FX (StableFX):** * `3.230.111.7` * `3.90.127.28` * `35.169.154.32` * `54.88.227.75` Register your endpoint as a subscriber by calling the Create Subscription endpoint for your product. Select your product below for the request shape: Wallets and Contracts share the same subscription endpoint. See the Create Subscription reference for [Wallets](/api-reference/wallets/common/create-subscription) or [Contracts](/api-reference/contracts/common/create-subscription) for the full schema. ```bash theme={null} curl --request POST \ --url https://api.circle.com/v2/notifications/subscriptions \ --header "Authorization: Bearer $CIRCLE_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "endpoint": "https://your-app.example.com/webhooks", "notificationTypes": ["*"] }' ``` Example response: ```json theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "name": "Transactions Webhook", "endpoint": "https://your-app.example.com/webhooks", "enabled": true, "createDate": "2026-01-15T21:47:35.107250Z", "updateDate": "2026-01-15T21:47:35.107250Z", "notificationTypes": ["*"], "restricted": false } } ``` CPN requires `name` and `enabled` in the request body in addition to the common fields. See the [Create Subscription](/api-reference/cpn/common/create-subscription) reference for the full schema. ```bash theme={null} curl --request POST \ --url https://api.circle.com/v2/cpn/notifications/subscriptions \ --header "Authorization: Bearer $CIRCLE_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "endpoint": "https://your-app.example.com/webhooks", "name": "CPN Webhooks", "enabled": true, "notificationTypes": ["*"] }' ``` Example response: ```json theme={null} { "data": { "id": "1609aa1c-510a-448d-b9b9-3a13566ff922", "name": "CPN Webhooks", "endpoint": "https://your-app.example.com/webhooks", "enabled": true, "createDate": "2026-01-15T21:47:35.107250Z", "updateDate": "2026-01-15T21:47:35.107250Z", "notificationTypes": ["*"], "restricted": false } } ``` See the [Create Subscription](/api-reference/stablefx/all/create-subscription) reference for the full schema. ```bash theme={null} curl --request POST \ --url https://api.circle.com/v2/stablefx/notifications/subscriptions \ --header "Authorization: Bearer $CIRCLE_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "endpoint": "https://your-app.example.com/webhooks", "notificationTypes": ["*"] }' ``` Example response: ```json theme={null} { "data": { "id": "c4d1da72-111e-4d52-bdbf-2e74a2d803d5", "name": "Transactions Webhook", "endpoint": "https://your-app.example.com/webhooks", "enabled": true, "createDate": "2026-01-15T21:47:35.107250Z", "updateDate": "2026-01-15T21:47:35.107250Z", "notificationTypes": ["*"], "restricted": false } } ``` Gateway uses a subscription that includes the wallet addresses and blockchain domains to monitor. See the [Create Subscription](/api-reference/gateway/all/create-permissionless-subscription) reference for the full schema. ```bash theme={null} curl --request POST \ --url https://api.circle.com/v2/notifications/subscriptions/permissionless \ --header "Authorization: Bearer $CIRCLE_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "environment": "mainnet", "endpoint": "https://your-app.example.com/webhooks", "addresses": ["0xYourWalletAddress"], "domains": [0], "notificationTypes": ["gateway.*"] }' ``` Example response: ```json theme={null} { "data": { "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636", "name": "Gateway Webhooks", "endpoint": "https://your-app.example.com/webhooks", "environment": "mainnet", "enabled": true, "addresses": ["0xYourWalletAddress"], "domains": [0], "notificationTypes": ["gateway.*"], "createDate": "2026-01-15T21:47:35.107250Z", "updateDate": "2026-01-15T21:47:35.107250Z" } } ``` Used by [Circle Mint](/circle-mint), [Digital Asset Accounts](/digital-asset-accounts), and [CPN Managed Payments](/cpn/managed-payments). Expose a publicly accessible HTTPS endpoint that: * Is reachable from the public internet. * Handles both `HEAD` and `POST` requests. Circle issues `HEAD` requests as a connectivity warmup. SNS deliveries arrive as `POST` requests with the SNS message in the request body. * Responds with a `2xx` status code so SNS treats the delivery as successful. To test before deploying a real endpoint, generate a temporary URL with [webhook.site](https://webhook.site/), use it as your subscriber endpoint, and copy the `SubscribeURL` from the confirmation message into your browser to complete the handshake. Call `POST /v1/notifications/subscriptions` with your endpoint URL. The same request shape works for Circle Mint, Digital Asset Accounts, and CPN Managed Payments. ```bash theme={null} curl --request POST \ --url https://api.circle.com/v1/notifications/subscriptions \ --header "Authorization: Bearer $CIRCLE_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "endpoint": "https://your-app.example.com/webhooks" }' ``` Example response: ```json theme={null} { "data": { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "endpoint": "https://your-app.example.com/webhooks", "subscriptionDetails": [ { "url": "arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic", "arn": "arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic:fcb4a2c9-9c4f-4706-b312-6b22650f5d17", "status": "pending" } ] } } ``` The subscription `status` is `pending` until you complete the confirmation handshake in the next step. v1 subscriptions deliver all account events; filtering by `notificationTypes` isn't supported. Circle Mint customers can also register subscriptions through the [Circle Mint console](https://app.circle.com/) under **Developer → Subscriptions**. After you register the subscription, SNS sends a `POST` to your endpoint with `Type: SubscriptionConfirmation`. The body includes a `SubscribeURL`. Open it in your browser, or have your endpoint fetch it server-side, to finish the handshake. The subscription status moves to `confirmed` and events begin flowing. Example `SubscriptionConfirmation` payload: ```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=...", "Timestamp": "2026-04-11T20:50:16.324Z", "SignatureVersion": "1", "Signature": "...", "SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-...pem" } ``` List active subscriptions with `GET /v1/notifications/subscriptions` and remove one with `DELETE /v1/notifications/subscriptions/{id}`. A subscription can be deleted only when every entry in `subscriptionDetails[]` is `confirmed`, `deleted`, or a mix of the two. A subscription with any `pending` entry cannot be deleted. Resolve the pending state first. | Environment | Active subscription cap | `pending` auto-removal | | ----------- | ----------------------- | ---------------------- | | Sandbox | 3 | After 30 days | | Production | 1 | After 72 hours | v1 traffic originates from Amazon SNS rather than Circle, so the IP range is the published [AWS SNS IP range](https://docs.aws.amazon.com/general/latest/gr/aws-ip-ranges.html) and not practical to allowlist narrowly. Rely on [signature verification](/api-reference/verify-webhook-signatures) to confirm authenticity. # Webhooks Source: https://developers.circle.com/api-reference/webhooks Learn how Circle uses webhooks to notify your application when events occur, including the event model, delivery behavior, ordering, and idempotency. Circle uses webhooks to notify your application when events occur. Circle products handle many operations in the background. When a resource changes state, Circle sends an HTTP `POST` to the endpoint you configure so your application can react. ## Notification API versions Circle offers two notification systems. Which one you integrate with depends on the product you're using. | Version | Delivery | Products | | ------- | ------------------------------------- | ----------------------------------------------------------------- | | **v2** | Direct HTTPS POST from Circle | Circle Wallets, Circle Contracts, CPN payments, Gateway, StableFX | | **v1** | Amazon SNS publishes to your endpoint | Circle Mint, Digital Asset Accounts, CPN Managed Payments | The two versions differ in subscription setup, message format, and signature verification. See [Set up a webhook endpoint](/api-reference/webhook-endpoints) for the flow that matches your product. ## Event model Each webhook notification is an HTTP `POST` request to a subscriber endpoint you configure. The envelope shape depends on the notification API version. Used by Circle Wallets, Circle Contracts, CPN payments, Gateway, and StableFX. Each notification includes: | Field | Type | Description | | ------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | `subscriptionId` | string (UUIDv4) | Identifies the subscription that produced the notification. Use it to route events to the correct handler. | | `notificationId` | string (UUIDv4) | Uniquely identifies the notification. The same ID is reused if Circle retries delivery, so use it to deduplicate. | | `notificationType` | string | The event type (for example, `transactions.inbound`, `cpn.payment.completed`). | | `notification` | object | The resource that changed, including its current state. The shape matches the corresponding API endpoint's response object. | | `timestamp` | string | ISO 8601 timestamp of the event. | | `version` | number | Schema version. Always `2`. | You subscribe to the events your application cares about. When a corresponding state change occurs in Circle's systems, Circle sends a notification to your endpoint. Used by Circle Mint, Digital Asset Accounts, and CPN Managed Payments. v1 uses Amazon Simple Notification Service (SNS) as the delivery layer, so each notification arrives as an SNS message wrapping the Circle event payload. The outer SNS envelope includes: | Field | Type | Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `Type` | string | The SNS message type. `Notification` for events, `SubscriptionConfirmation` for the one-time handshake described below. | | `MessageId` | string | Unique identifier for this delivery. Reused if SNS retries, so use it to deduplicate. | | `TopicArn` | string | The SNS topic the subscription is bound to. | | `Message` | string | The Circle event payload, encoded as a JSON string. Parse it to access the inner envelope described next. | | `Signature` | string | Base64-encoded signature of the canonical message string. | | `SigningCertURL` | string | URL of the public certificate used to verify `Signature`. See [Verify webhook signatures](/api-reference/verify-webhook-signatures). | The inner Circle envelope, parsed from `Message`, includes: | Field | Type | Description | | ------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `clientId` | string (UUIDv4) | Identifies the Circle account that owns the resource. | | `notificationType` | string | The event topic (for example, `deposits`, `transfers`). The topic determines which resource key is present on the envelope. | | `version` | number | Schema version for the notification payload. | | `customAttributes` | object | Echoes selected envelope attributes such as `clientId`. | | `` | object | The topic-specific resource (for example, `deposit`, `transfer`, `payout`) carrying the current state of the changed object. | When you register a v1 subscription, your endpoint first receives a one-time `SubscriptionConfirmation` message containing a `SubscribeURL`. Visit that URL to complete the handshake before events begin flowing. See [Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications) for the full flow. ## Delivery, ordering, and idempotency Circle delivers webhook notifications at least once. If your endpoint does not respond with a success status, or if the request fails, Circle retries delivery. The same notification can be sent more than once. Keep in mind the following: * **Your endpoint must be idempotent:** Your handler must produce the same result each time it runs. Deduplicate on the `Notification ID` (v2) or `MessageId` (v1) before applying side effects. * **Your application must not assume delivery order:** Notifications for successive state changes (for example, `CONFIRMED` then `COMPLETE`) can arrive in a different order than when the state changes occurred. Handle each notification based on the state it carries, not on its arrival sequence. To inspect delivery attempts, view payloads, or resend a notification, view Webhook Logs in the [Circle Console](https://console.circle.com) (Wallets, Contracts) or [CPN Console](https://cpn.circle.com) (CPN). # xReserve API Source: https://developers.circle.com/api-reference/xreserve Deposit USDC into xReserve, retrieve attestations, and manage withdrawals for USDC-backed stablecoins. xReserve is the interoperability layer behind your own USDC-backed stablecoin. Circle holds the underlying reserves on source blockchains such as Ethereum and exposes the state and lifecycle to you through the API. ## Get started Authenticate your requests with an API key. Learn how xReserve works. ## Endpoint categories List, retrieve, and look up attestations by transaction hash. Read reserve balances and reserve info. Prepare, submit, and track withdrawals. ## OpenAPI specifications The xReserve API reference is generated from this OpenAPI specification: * `https://developers.circle.com/openapi/xreserve.yaml` See [OpenAPI Specifications](/api-reference/openapi-specifications) for the full catalog of Circle's API specs. # 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, cirBTC for tokenized Bitcoin, 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. ## Wrapped assets Tokenized versions of major crypto assets, backed 1:1 by their underlying asset. ## 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. * Learn about [cirBTC](/assets/what-is-cirbtc) and how to use wrapped Bitcoin in onchain finance. * 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. # cirBTC contract addresses Source: https://developers.circle.com/assets/cirbtc-contract-addresses Smart contract addresses for cirBTC (Circle Wrapped Bitcoin) on supported mainnet and testnet blockchains. cirBTC tokens are controlled by a smart contract on the blockchain. The following sections list the cirBTC smart contract addresses on every supported mainnet and testnet blockchain. ## Mainnet **Mainnet tokens have financial value** The blockchains listed below store and transfer tokens that have real financial value. When interacting with mainnet blockchains, you should thoroughly test your code, verify all addresses, and ensure the privacy of seed phrases and private keys. | Blockchain | cirBTC Mainnet Address | | :--------- | :-------------------------------------------------------------------------------------------------------------------- | | Ethereum | [`0x72DFB2E44f59C5AD2bAFE84314E5b99a7cd5075E`](https://etherscan.io/token/0x72dfb2e44f59c5ad2bafe84314e5b99a7cd5075e) | ## Testnet **Testnet tokens have no financial value** Because the testnets listed below are used only for testing, the cirBTC tokens in circulation on these networks have no financial value, and **are not backed by real Bitcoin**. Similarly, native testnet tokens have no financial value. | Blockchain | Token Address | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Arc Testnet | [`0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF`](https://testnet.arcscan.app/address/0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF) | | Ethereum Sepolia | [`0x3a3fe695F684Bf9b9e43CF43C2b895Ea5e392bB3`](https://sepolia.etherscan.io/address/0x3a3fe695F684Bf9b9e43CF43C2b895Ea5e392bB3) | # What is cirBTC? Source: https://developers.circle.com/assets/what-is-cirbtc Circle Wrapped Bitcoin (cirBTC) is a 1:1 BTC-backed token issued by Circle on Ethereum with real-time onchain reserve verification through Chainlink Proof of Reserves. cirBTC is Circle Wrapped Bitcoin—a tokenized representation of Bitcoin (BTC) issued by Circle on Ethereum. Every cirBTC is backed 1:1 by native BTC held at a regulated entity in the Circle group of companies. The BTC is held for the exclusive benefit of cirBTC holders and segregated from Circle's corporate assets. ## Why cirBTC? Bitcoin is the most widely held digital asset, yet it cannot natively interact with smart contracts, limiting its use in lending, borrowing, or complex trading strategies. cirBTC solves this by creating a tokenized version of BTC that operates on smart contract blockchains, transforming idle Bitcoin into productive capital. * **1:1 backed by native BTC**: every cirBTC is redeemable for the underlying Bitcoin. * **Real-time proof of reserves**: onchain verification through Chainlink rather than periodic attestations. * **Neutral issuer**: Circle does not operate a competing exchange or lending protocol. Paired with USDC, cirBTC enables institutional workflows such as posting cirBTC as collateral to borrow USDC and access capital without selling the underlying Bitcoin position. ## Access and availability cirBTC is available to qualified businesses through [Circle Mint](/circle-mint). [Contact Circle](https://www.circle.com/mint-contact) to learn more. Mint and redeem cirBTC using the same API endpoints as USDC and EURC, with `CIRBTC` as the currency code. For step-by-step instructions, see the [Mint and redeem cirBTC quickstart](/circle-mint/quickstarts/mint-and-redeem-cirbtc). cirBTC is available on the following blockchains: * **Ethereum**: mainnet and Sepolia testnet * **Arc**: testnet Developers can test cirBTC integrations using the [Circle faucet](https://faucet.circle.com/) to obtain testnet cirBTC, and the [sandbox environment](https://app-smokebox.circle.com) to test API interactions. ## cirBTC in the API cirBTC uses the currency code `CIRBTC` in all Circle Mint API calls. You interact with it the same way you use `USD` (for USDC) or `EUR` (for EURC) when creating transfers, deposit addresses, and other operations. For example, to create a deposit address for cirBTC on Ethereum: ```bash theme={null} curl -X POST https://api.circle.com/v1/businessAccount/wallets/addresses/deposit \ -H "Authorization: Bearer ${YOUR_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"idempotencyKey":"unique-uuid","currency":"CIRBTC","chain":"ETH"}' ``` For supported blockchains and blockchain codes, see [Supported chains and currencies](/circle-mint/references/supported-chains-and-currencies). To get started with your first mint or redemption, see the [Mint and redeem cirBTC quickstart](/circle-mint/quickstarts/mint-and-redeem-cirbtc). # 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 social logins, email OTP, and PIN-secured signing so your users control their keys with familiar sign-ins. 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 social login, email OTP, and PIN-based signing flows. * 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 three contracts depending on the source chain and integration pattern: * `TokenMessengerV2` contract * `CctpExtension` contract * `CctpExtensionV2` contract (Arbitrum only; sponsored / relayer-submitted deposits) 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. ### Sponsored deposits on Arbitrum `CctpExtensionV2` is a separate Arbitrum contract from `CctpExtension`. It enables gas-sponsored CCTP burns: the end user signs a single EIP-3009 `ReceiveWithAuthorization` offchain, and a relayer submits the Arbitrum transaction via `batchSponsorDepositForBurn`, paying ETH gas on the user's behalf. Use this path when your users hold USDC on Arbitrum but not native gas (for example, email-login or CEX-funded wallets). Most integrators who control their own wallet and gas should continue using `CctpExtension` or `TokenMessengerV2` as documented in [Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore). Key differences from `CctpExtension`: 1. The relayer, not the depositor, broadcasts the burn transaction. 2. The EIP-3009 authorization nonce is deterministically derived from the deposit parameters (destination, recipient, fees, hook data), which binds the user's signature to the intended CCTP transfer. 3. The relayer may batch multiple user deposits in one transaction. After the burn, the crosschain path is the same as other Arbitrum → HyperCore transfers: CCTP attestation, mint on HyperEVM, optional `CctpForwarder` hook to HyperCore. For contract addresses, see [HyperCore CCTP-Enablement Contract Addresses](/cctp/references/hypercore-contract-addresses). For the contract interface, see [CctpExtensionV2 Contract Interface](/cctp/references/cctp-extension-v2-contract-interface). For integration steps, see [Transfer USDC from Arbitrum to HyperCore with CctpExtensionV2](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2). 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-13 basis points depending on source blockchain, for example \$0–\$1.30 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.4 bps (0.014%) | | 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 | 13 bps (0.13%) | | Morph | 4 bps (0.04%) | | OP Mainnet | 1.3 bps (0.013%) | | Plume | 2 bps (0.02%) | | Solana | 1 bps (0.01%) | | Starknet | 12 bps (0.12%) | | Unichain | 2 bps (0.02%) | | World Chain | 1.3 bps (0.013%) | | X Layer | 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%) | | Cronos | 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%) | | X Layer | 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. ## Charging your own application fee The fees described on this page are protocol fees collected by Circle. The CCTP protocol has no built-in mechanism for adding a custom application or marketplace fee to a transfer. To collect a fee from your users on top of the protocol fee, either use [Bridge Kit's custom fee support](https://docs.arc.io/app-kit/tutorials/bridge/collect-bridge-fee) or implement the fee yourself in a smart contract or backend alongside the CCTP call (for example, by wrapping `depositForBurn` in a contract that also transfers a fee to your treasury). # 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 | | **X Layer** | 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 | | **Cronos** | 1 | \~0.5 seconds | | **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 | | **X Layer** | \~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, World Chain, and X Layer) 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 owner 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. In Solana terms, the recipient owner can be an on-curve public key controlled by a keypair, such as a wallet public key, or an off-curve Program Derived Address (PDA) controlled by a Solana program. If the owner might be off-curve, pass `true` for `allowOwnerOffCurve`: ```typescript theme={null} import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; const recipientOwner = new PublicKey("RecipientOwnerAddress"); const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet const recipientAta = getAssociatedTokenAddressSync( USDC_MINT, recipientOwner, true, ); 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 owner address for the ATA (on-curve or off-curve) | | 65+ | `any` | Developer-defined hook data | When using this format, `mintRecipient` must be the ATA derived from the recipient owner address in bytes 33-64 and the USDC mint. The recipient owner can be an on-curve public key controlled by a keypair or an off-curve PDA. Forwarding Service validates that these values match 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 owner address for the ATA (32 bytes) const recipientOwner = new PublicKey("RecipientOwnerAddress"); const ownerBytes = Buffer.from(recipientOwner.toBytes()); const forwardHookData = "0x" + Buffer.concat([magicBytes, version, length, ataFlag, ownerBytes]).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: | Forwarding route | Service fee (USDC) | | --------------------------------------------------- | ------------------ | | HyperCore deposits | \$0.20 | | HyperCore withdrawals to Ethereum | \$1.20 | | HyperCore withdrawals to Solana | \$0.50 | | HyperCore withdrawals to all other supported chains | \$0.20 | | All other forwarding destinations | \$0.05 | 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. **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 is not necessary. These chains are marked **N/A** in the Source (Fast transfer) column below. | Blockchain | Source (Standard transfer) | Source (Fast transfer) | Forwarding Service | | --------------------------- | -------------------------- | ---------------------- | ------------------ | | Arbitrum | ✅ | ✅ | ✅ | | Arc testnet | ✅ | N/A | ✅ | | Avalanche | ✅ | N/A | ✅ | | Base | ✅ | ✅ | ✅ | | BNB Smart Chain (USYC only) | ✅ | N/A | ❌ | | Codex | ✅ | ✅ | ✅ | | Cronos | ✅ | N/A | ❌ | | EDGE | ✅ | ✅ | ✅ | | Ethereum | ✅ | ✅ | ✅ | | HyperEVM | ✅ | N/A | ✅ | | Injective | ✅ | N/A | ❌ | | Ink | ✅ | ✅ | ✅ | | Linea | ✅ | ✅ | ✅ | | Monad | ✅ | N/A | ✅ | | Morph | ✅ | ✅ | ❌ | | OP Mainnet | ✅ | ✅ | ✅ | | Pharos | ✅ | N/A | ❌ | | Plume | ✅ | ✅ | ✅ | | Polygon PoS | ✅ | N/A | ✅ | | Sei | ✅ | N/A | ✅ | | Solana | ✅ | ✅ | ✅ | | Sonic | ✅ | N/A | ✅ | | Starknet | ✅ | ✅ | ❌ | | Stellar | ✅ | N/A | ❌ | | Unichain | ✅ | ✅ | ✅ | | World Chain | ✅ | ✅ | ✅ | | X Layer | ✅ | ✅ | ❌ | | XDC | ✅ | N/A | ✅ | 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. Arc is the exception: CCTP supports Arc testnet only. ## 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 | | 32 | Cronos | | 37 | X Layer | ## 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.6+](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 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} node --env-file=.env script.ts ``` # How-to: Transfer USDC from Arbitrum to HyperCore with CctpExtensionV2 Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2 Submit a gas-sponsored CCTP deposit to HyperCore using EIP-3009 and a relayer. Submit a gas-sponsored CCTP deposit from Arbitrum to HyperCore using the `CctpExtensionV2` contract. This workflow suits users who hold USDC on Arbitrum but lack native gas tokens. A relayer submits the Arbitrum transaction on their behalf. For self-custody integrators who pay their own gas, use [Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore) with `CctpExtension` instead. For background on the contract, see [CctpExtensionV2 Contract Interface](/cctp/references/cctp-extension-v2-contract-interface). ## Prerequisites Before you begin, ensure that you've: * Deployed or operate a relayer that can submit transactions on Arbitrum and pay ETH gas * Collected an EIP-3009 `ReceiveWithAuthorization` signature from each depositor * Identified the `CctpExtensionV2` proxy address for your environment (see [HyperCore contract addresses](/cctp/references/hypercore-contract-addresses)) * Configured CCTP deposit parameters for HyperCore (HyperEVM domain `19`, `CctpForwarder` as `mintRecipient` / `destinationCaller`, and forwarder hook data for the HyperCore recipient) ## Steps ### Step 1. Collect the user's EIP-3009 signature The user signs `ReceiveWithAuthorization` with: * `from`: depositor address * `to`: `CctpExtensionV2` proxy address * `value`: total USDC amount covered by this authorization * `validAfter` / `validBefore`: authorization time window * `nonce`: deterministic value equal to `keccak256(abi.encode(from, amount, validAfter, validBefore, depositData))`, where `depositData` is the ABI-encoded `SponsoredDepositForBurnData` struct for this deposit Binding the nonce to `depositData` prevents a relayer from redirecting funds to a different destination or fee configuration than the user signed. ### Step 2. Submit the deposit via your relayer Call `batchSponsorDepositForBurn` on the `CctpExtensionV2` proxy with: * `_authData`: `ReceiveWithAuthorizationDataWithFrom[]` (includes explicit `from`) * `_depositData`: `SponsoredDepositForBurnData[]` with CCTP fields aligned to your HyperCore destination Use the same forwarder hook encoding as [Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore). You may batch multiple users in one transaction. Entries with an already-consumed nonce or insufficient balance are skipped (`SponsoredDepositForBurnSkipped` event) without reverting the whole batch. ### Step 3. Monitor the crosschain transfer After the Arbitrum transaction confirms, Circle's attestation service picks up the burn event. The `CctpForwarder` on HyperEVM receives the minted USDC and routes it to the HyperCore recipient via the forwarder hook. No special handling is required beyond standard CCTP and forwarder monitoring. For details, see [CCTP on HyperCore](/cctp/concepts/cctp-on-hypercore). # 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.6+](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 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} node --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.6+](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": 56035, // 0.056035 USDC "med": 57543, // 0.057543 USDC "high": 59052 // 0.059052 USDC } }, { "finalityThreshold": 2000, // Standard transfer "minimumFee": 0, // No fee "forwardFee": { "low": 56035, // 0.056035 USDC "med": 57543, // 0.057543 USDC "high": 59052 // 0.059052 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.058843 USDC (0.057543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol fee). For the recipient to receive 10 USDC, you must burn 10.058843 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} node --env-file=.env 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) | | `expirationBlock` has passed before you submitted the mint | Attestations encode an `expirationBlock` 24 hours in the future. Once that block passes on the destination blockchain, the existing attestation can no longer be used to mint. | Call [`POST /v2/reattest/{nonce}`](/api-reference/cctp/all/reattest-message) to request a new attestation with a refreshed `expirationBlock`, then poll [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) and re-mint. | **Expired messages are not permanently stuck** There is no deadline after which re-attestation becomes unavailable. As long as the original burn transaction still exists on the source blockchain, you can request a fresh attestation at any time using the [re-attest endpoint](/api-reference/cctp/all/reattest-message). # 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.6+](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 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 ID of the chain used when signing in hexadecimal format (for example, `"0xa4b1"` for Arbitrum). * `token`: `USDC` * `amount`: The amount of USDC as a string (for example, `"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", // Chain ID used when 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` (the ID of the chain used when signing in hexadecimal format) 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", // Chain ID used when 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} node --env-file=.env 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.6+](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 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} node --env-file=.env script.ts ``` # How-to: Withdraw USDC from HyperCore to Solana Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-solana Withdraw USDC from a HyperCore spot or perp balance to a Solana associated token account using CCTP. Withdraw USDC from a HyperCore `spot` or `perp` balance directly to a Solana associated token account (ATA) using CCTP. The withdrawal burns USDC on HyperEVM and mints it on Solana in a single operation. For withdrawals to EVM blockchains, see [Withdraw USDC from HyperCore to EVM chains](./withdraw-usdc-from-hypercore-to-evm). For CCTP domain IDs, see [Supported blockchains and domains](../concepts/supported-chains-and-domains). ## 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 @solana/web3.js @solana/spl-token npm install -D tsx typescript @types/node ``` * Created a `.env` file with required environment variables. Open the file in your editor and add your private key and Solana addresses: ```text theme={null} PRIVATE_KEY=0x... SOLANA_ATA_OWNER=... # Solana wallet address that owns the recipient ATA SOLANA_USDC_MINT=... # USDC mint address on the target Solana network ``` Keep your private key safe and never commit `.env` to version control. Add `.env` to your `.gitignore` file. Withdrawals to Solana have a higher CCTP forwarding fee for handling Associated Token Account creation. If the withdrawal amount is less than this fee, the transaction reverts on HyperEVM. ## Steps ### Step 1. Derive the recipient ATA and createATA hook data For Solana, the mint recipient is the associated token account for the USDC mint and recipient owner. The `createATA` hook data tells CCTP to create that ATA when needed. ```ts TypeScript theme={null} import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; function encodeCreateAtaHookData(ataOwner: PublicKey): `0x${string}` { const magic = Buffer.from("cctp-forward", "utf-8") .toString("hex") .padEnd(48, "0"); const createAta = "01"; const owner = Buffer.from(ataOwner.toBytes()).toString("hex"); return `0x${magic}${createAta}${owner}`; } const ataOwner = new PublicKey(process.env.SOLANA_ATA_OWNER as string); const usdcMint = new PublicKey(process.env.SOLANA_USDC_MINT as string); const recipientAta = getAssociatedTokenAddressSync(usdcMint, ataOwner); const createAtaHookData = encodeCreateAtaHookData(ataOwner); ``` ### Step 2. Construct the `sendToEvmWithData` action Create a `sendToEvmWithData` action object with the following parameters: * `type`: `sendToEvmWithData` * `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet) * `signatureChainId`: The ID of the chain used when signing in hexadecimal format (for example, `"0xa4b1"` for Arbitrum). * `token`: `USDC` * `amount`: The amount of USDC as a string (for example, `"10"` for 10 USDC, `"1.5"` for 1.5 USDC) * `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for `perp` balance * `destinationRecipient`: The recipient ATA on Solana. This is the ATA for the recipient owner and the USDC mint, not the owner wallet address. * `addressEncoding`: `base58` for Solana recipients * `destinationChainId`: `5`, the CCTP destination domain ID for Solana * `gasLimit`: Gas limit for the transaction on the destination blockchain * `data`: The `createATA` hook data from the previous step, which includes the recipient owner public key as `ataOwner`. The CCTP `destinationCaller` is always set to the zero address, so any caller can submit the CCTP receive instruction on Solana. The USDC still mints only to `destinationRecipient`. * `nonce`: Current timestamp in milliseconds ```ts TypeScript theme={null} // Example action payload for sendToEvmWithData const action = { type: "sendToEvmWithData", hyperliquidChain: "Mainnet", signatureChainId: "0xa4b1", // Chain ID used when signing token: "USDC", amount: "10", // 10 USDC sourceDex: "spot", // or "" for perp destinationRecipient: recipientAta.toBase58(), addressEncoding: "base58", destinationChainId: 5, // Solana CCTP domain gasLimit: 200000, data: createAtaHookData, nonce: Date.now(), }; ``` ### Step 3. 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 = Number(BigInt(action.signatureChainId)); 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: recipientAta.toBase58(), addressEncoding: "base58", destinationChainId: 5, gasLimit: BigInt(200000), data: createAtaHookData, 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 4. 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: action.nonce, 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 Solana. By default, it withdraws 10 USDC from your perp balance. Update `destinationChainId` only if the CCTP Solana domain changes, and set `signatureChainId` to the ID of the chain used when signing in hexadecimal format. ```ts TypeScript expandable theme={null} /** * Script: Withdraw USDC from HyperCore to Solana * - Derives the recipient associated token account * - Encodes createATA hook data * - Signs EIP-712 sendToEvmWithData action * - Submits to Hyperliquid /exchange API */ import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { Wallet, Signature } from "ethers"; function encodeCreateAtaHookData(ataOwner: PublicKey): `0x${string}` { const magic = Buffer.from("cctp-forward", "utf-8") .toString("hex") .padEnd(48, "0"); const createAta = "01"; const owner = Buffer.from(ataOwner.toBytes()).toString("hex"); return `0x${magic}${createAta}${owner}`; } // -------- 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 // Solana destination parameters ataOwner: process.env.SOLANA_ATA_OWNER as string, usdcMint: process.env.SOLANA_USDC_MINT as string, destinationChainId: 5, // Solana CCTP domain addressEncoding: "base58", gasLimit: Number(process.env.GAS_LIMIT || 200000), // Hyperliquid environment isMainnet: String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true", signatureChainId: "0xa4b1", // Chain ID used when signing }; // -------- Main Function -------- async function main() { // Validate required parameters if (!config.privateKey) { throw new Error("Set PRIVATE_KEY"); } if (!config.ataOwner) { throw new Error("Set SOLANA_ATA_OWNER"); } if (!config.usdcMint) { throw new Error("Set SOLANA_USDC_MINT"); } const ataOwner = new PublicKey(config.ataOwner); const usdcMint = new PublicKey(config.usdcMint); const recipientAta = getAssociatedTokenAddressSync(usdcMint, ataOwner); const createAtaHookData = encodeCreateAtaHookData(ataOwner); 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("Solana ATA owner:", ataOwner.toBase58()); console.log("Destination recipient ATA:", recipientAta.toBase58()); 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: recipientAta.toBase58(), addressEncoding: config.addressEncoding, destinationChainId: config.destinationChainId, gasLimit: BigInt(config.gasLimit), data: createAtaHookData, 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: recipientAta.toBase58(), addressEncoding: config.addressEncoding, destinationChainId: config.destinationChainId, gasLimit: config.gasLimit, data: createAtaHookData, 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 ``` # 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.6+](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="node --env-file=.env index.ts" # Install runtime dependencies npm install viem # Install dev dependencies npm install --save-dev @types/node 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` in the `burnUSDC` function preceding. ```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 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five 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.6+](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="node --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 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")], }); // Generate the MessageSent event account passed into the burn instruction as // a signer, where the MessageTransmitterV2 program stores the MessageSent event. 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")], }); // Generate the MessageSent event account passed into the burn instruction as // a signer, where the MessageTransmitterV2 program stores the MessageSent event. 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")], }); // Generate the MessageSent event account passed into the burn instruction as // a signer, where the MessageTransmitterV2 program stores the MessageSent event. 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")], }); // Generate the MessageSent event account passed into the burn instruction as // a signer, where the MessageTransmitterV2 program stores the MessageSent event. 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 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five 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.6+](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="node --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` in the `burnUSDC` function preceding. ```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 stop responding at `Waiting for attestation...`, allow at least five minutes before investigating. **Rate limit:** The attestation service rate limit is 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five 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.6+](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="node --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` preceding. ```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 stop responding at `Waiting for attestation...`, allow at least five minutes before investigating. **Rate limit:** The attestation service rate limit is 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five 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); ``` # CctpExtensionV2 Contract Interface Source: https://developers.circle.com/cctp/references/cctp-extension-v2-contract-interface Sponsored CCTP deposits from Arbitrum using EIP-3009 and a relayer. `CctpExtensionV2` is an optional Arbitrum contract for sponsored CCTP deposits to HyperCore. It is separate from [`CctpExtension`](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore), which expects the depositor to submit the burn transaction and pay gas. Circle deploys and maintains the contract. Circle does not operate the source-chain relayer: the integrator (or their partner) runs relayer infrastructure that submits Arbitrum transactions on users' behalf. ## What it does 1. The user signs an offchain EIP-3009 `ReceiveWithAuthorization` that authorizes the contract to pull USDC from their wallet. 2. A relayer calls `batchSponsorDepositForBurn` with parallel arrays of authorization data and CCTP deposit parameters, paying Arbitrum gas. 3. The contract validates that each authorization nonce matches the intended deposit parameters, pulls USDC, and burns via CCTP `TokenMessengerV2`. 4. CCTP attestation and destination forwarding proceed like any other Arbitrum → HyperEVM → HyperCore transfer (including optional `CctpForwarder` hook data). Primary entry point: `batchSponsorDepositForBurn` ## Who should use it | Integrator profile | Recommended path | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Users hold ETH on Arbitrum and submit their own transactions | [`CctpExtension`](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore) or `TokenMessengerV2` | | Users hold USDC but not native gas; you operate (or partner with) a relayer that pays gas | [`CctpExtensionV2`](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2) | | Transfers from chains other than Arbitrum | `TokenMessengerV2` + HyperCore hook data (no `CctpExtensionV2` deployment) | `CctpExtensionV2` is deployed on Arbitrum only. It exists to support relayer-based deposit UX for a subset of Hyperliquid users; it is not a general-purpose replacement for `CctpExtension`. ## Contract addresses See [HyperCore CCTP-Enablement Contract Addresses](/cctp/references/hypercore-contract-addresses#cctpextensionv2-mainnet) (mainnet) and [CctpExtensionV2 testnet addresses](/cctp/references/hypercore-contract-addresses#cctpextensionv2-testnet). ## How-to For integration steps, see [Transfer USDC from Arbitrum to HyperCore with CctpExtensionV2](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2). # 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) | | **Cronos** | 32 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.cronos.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) | | **X Layer** | 37 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://www.okx.com/web3/explorer/xlayer/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) | | **Cronos** | 32 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.cronos.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) | | **X Layer** | 37 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://www.okx.com/web3/explorer/xlayer/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) | | **Cronos** | 32 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.cronos.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) | | **X Layer** | 37 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://www.okx.com/web3/explorer/xlayer/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) | | **Cronos** | 32 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.cronos.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) | | **X Layer** | 37 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://www.okx.com/web3/explorer/xlayer/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) | | **Cronos Testnet** | 32 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer.cronos.org/testnet/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) | | **X Layer Testnet** | 37 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://www.okx.com/web3/explorer/xlayer-test/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) | | **Cronos Testnet** | 32 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer.cronos.org/testnet/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) | | **X Layer Testnet** | 37 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://www.okx.com/web3/explorer/xlayer-test/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) | | **Cronos Testnet** | 32 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer.cronos.org/testnet/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) | | **X Layer Testnet** | 37 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://www.okx.com/web3/explorer/xlayer-test/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) | | **Cronos Testnet** | 32 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer.cronos.org/testnet/address/0xbaC0179bB358A8936169a63408C8481D582390C4) | | **X Layer Testnet** | 37 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://www.okx.com/web3/explorer/xlayer-test/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. * **`CctpExtensionV2`**: (Arbitrum only) Enables sponsored CCTP deposits from Arbitrum to HyperCore. A relayer submits `batchSponsorDepositForBurn` on behalf of users who signed EIP-3009 `ReceiveWithAuthorization` offchain. See [CctpExtensionV2 Contract Interface](/cctp/references/cctp-extension-v2-contract-interface). * **`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) | ### CctpExtensionV2: Mainnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | ------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Arbitrum** | 3 | [`0x3289e443a95B28Bcedacc4B33C689b0C9b84ffAB`](https://arbiscan.io/address/0x3289e443a95B28Bcedacc4B33C689b0C9b84ffAB) | `CctpExtensionV2` is separate from `CctpExtension`. For sponsored relayer deposits, see [Transfer USDC from Arbitrum to HyperCore with CctpExtensionV2](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2). For self-submitted burns, see [Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore). ### 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) | ### CctpExtensionV2: Testnet | Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address | | -------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Arbitrum Sepolia** | 3 | [`0xC1Fec4e5DAf8796A490653359536f9c2a9C158aE`](https://sepolia.arbiscan.io/address/0xC1Fec4e5DAf8796A490653359536f9c2a9C158aE) | ### 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 [Transfer USDC from Solana to Arc quickstart](/cctp/quickstarts/transfer-usdc-solana-to-arc) 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 | **Working with `expirationBlock`** On 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), so compare the `expirationBlock` value against the current Ethereum block number, not the L2 block number. **Expired Fast Transfer burns** An expired Fast Transfer burn is not permanently stuck. As long as the burn transaction still exists on the source blockchain, you can call [`POST /v2/reattest/{nonce}`](/api-reference/cctp/all/reattest-message) at any time to request a new attestation with a refreshed `expirationBlock`. There is no deadline after which re-attestation becomes unavailable. After re-attestation, poll [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) for the new attestation and submit the mint on the destination blockchain before the new `expirationBlock` passes. ## 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 40 requests per second. If you exceed 40 requests per second, the service blocks all API requests for the next five 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 40 requests per second. If you exceed 40 requests per second, the service blocks all API requests for the next five 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, EURC, and cirBTC directly from Circle. Circle Mint is for institutional customers minting USDC, EURC, or cirBTC. 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 and redeem [USDC](/stablecoins/what-is-usdc), EURC, and [cirBTC](/assets/what-is-cirbtc) directly from Circle. USDC and EURC redeem 1:1 for their backing fiat currency: U.S. dollars for USDC, euros for EURC. cirBTC redeems 1:1 for native BTC held at a regulated entity in the Circle group. 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. Deposit BTC to mint cirBTC, transfer it onchain, and redeem it back to BTC 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. # Credit API Source: https://developers.circle.com/circle-mint/concepts/credit-api Understand how the Credit API exposes Circle Mint's Settlement Advance and Line of Credit products, the credit-line model, and the lifecycle of credit transfers and repayments. The Credit API is the borrower-facing programmatic interface to Circle Mint's two credit facilities: Settlement Advance and Line of Credit. It exposes endpoints under `/v1/credit/` that you use to inspect your credit line, initiate draws, follow disbursement, and process repayments in fiat or crypto. For step-by-step procedural walkthroughs, see the [Settlement Advance quickstart](/circle-mint/quickstarts/settlement-advance) and the [Line of Credit quickstart](/circle-mint/quickstarts/line-of-credit). The Credit API requires three things before activation: an executed Core API Agreement, a separate offline credit contract with Circle, and the Credit API entitlement enabled on your account. To start the process, [contact Circle](https://www.circle.com/mint-contact) to discuss credit products and request that the Credit API entitlement be enabled. ## Two products: Settlement Advance and Line of Credit The Credit API surfaces two distinct products. A given customer holds one credit line for one product; the product determines how draws are requested, how fees accrue, and which repayment paths are available. | | Settlement Advance | Line of Credit | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Target use case | Covers settlement-timing gaps for customers on slow fiat rails, such as OTC desks, capital markets firms, and payment companies. You prefund a settlement and request the advance against it. | Provides fast, automated drawdowns for ongoing liquidity needs. Suited to customers who want programmatic access to working capital without per-draw manual review. | | Draw mechanics | Reserve-then-request flow: you reserve funds, wire USD to Circle, upload the wire proof, and Circle's Treasury team approves the disbursement. | Auto-approved single request: a `POST /v1/credit/transfers` call moves the transfer through `requested` and into `disbursed` without manual review. | | Repayment paths | Fiat only. You wire USD to a Circle-issued repayment account. | Fiat (wire) or crypto. Crypto repayments deduct USDC from your Circle Mint wallet. | | `feeCadence` options | `daily` only. Recurring fees accrue every 24 hours and repayment is due 7 days after disbursement. | `daily` or `hourly`. Hourly cadence accrues fees every hour with a 24-hour repayment window, sized for capital-markets workflows that need same-day repayment turns. | ## The credit-line model Your credit line is the facility object the Credit API revolves around. Every draw, fee, and repayment is recorded against it. `GET /v1/credit` returns the current state of the line. The subsections below explain the parts of that model you should be most familiar with. ### One credit line per customer, scoped to a product A Circle Mint customer has at most one credit line, and the line is scoped to a single `product`: either `settlementAdvance` or `lineOfCredit`. The settlement currency is USD. If your business needs both products, that requires a separate contractual conversation with Circle, not a second credit line on the same account. ### Fee rates The credit line's `feeRates` object captures the two rates that can apply to a draw: * `drawFee`: A one-time fee assessed at disbursement. Only Settlement Advance uses `drawFee`; Line of Credit does not charge a per-draw fee. * `recurringFee`: A fee that accrues against the outstanding balance at the cadence defined by `feeCadence`. Both products use `recurringFee`. Both rates are decimal multipliers applied to the transfer amount. For example, a `recurringFee` of `0.001` represents a 0.1% rate. Fees accrue automatically and are deducted from your Circle Mint wallet balance. ### Fee cadence and repayment timing `feeCadence` controls how often `recurringFee` accrues and how soon a disbursed transfer must be repaid: * `daily`: Fees accrue every 24 hours. Each disbursed transfer is due 7 days after disbursement. * `hourly`: Fees accrue every hour. Each disbursed transfer is due 24 hours after disbursement. Hourly cadence is available on Line of Credit only and is designed for trading and capital-markets workflows that recycle capital intraday. ### Minimum balance The credit line carries a `minBalance` requirement: the USDC balance you must keep in your Circle Mint wallet while the credit line is active. The minimum balance is an operating constraint that supports fee deduction and crypto repayment for Line of Credit customers; falling below it surfaces as a validation error that blocks new draws. ### Validation errors `validationErrors` is a list of blocking conditions on new transfer creation. When it is non-empty, the credit line cannot originate new transfers and the relevant create-transfer endpoints return HTTP 400 until the conditions clear. Three codes can appear: * `INSUFFICIENT_BALANCE`: The Circle Mint wallet's USDC balance is below `minBalance`. * `PENDING_FEES`: Accrued fees are still pending settlement against the wallet. * `OVERDUE_TRANSFERS`: At least one disbursed transfer has passed its due date without being fully repaid. Treat `validationErrors` as constraints to check before initiating a draw. The quickstarts cover how to resolve each condition operationally. ## Credit transfer lifecycle A credit transfer is one draw against your credit line. The two products follow different lifecycles because Settlement Advance involves a reserve-then-request handshake with manual Treasury approval, while Line of Credit auto-approves on request. ### Settlement Advance lifecycle A Settlement Advance transfer starts when you reserve funds and ends when the disbursed amount is fully repaid. The reservation step exists so you can lock in capacity against your line while you initiate the supporting wire. ```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 ``` * `funds_reserved`: Capacity is reserved against your credit line. The reservation expires after 30 minutes if it is not progressed to `requested`, and only one transfer at a time may be in this state per credit line. * `requested`: You have submitted wire proof for the reserved transfer and Circle's Treasury team is reviewing the request. * `disbursed`: Treasury has approved the advance and the funds have landed in your Mint wallet (or, with Credit Express, at the verified blockchain destination). * `paid`: The disbursed amount has been fully repaid. * `expired`: The reservation timed out before it was progressed to `requested`. * `canceled`: You canceled the reservation before submitting wire proof. * `rejected`: Treasury declined the request. * `past_due`: The disbursed amount has not been fully repaid by its due date. ### Line of Credit lifecycle A Line of Credit transfer skips the reservation step. A single create request moves the transfer to `requested` and Circle auto-disburses it. ```mermaid theme={null} stateDiagram-v2 [*] --> requested requested --> disbursed requested --> rejected disbursed --> paid disbursed --> past_due ``` * `requested`: The draw has been created and is being processed for disbursement. * `disbursed`: Funds have landed in your Mint wallet (or at the verified blockchain destination when Credit Express is configured). * `paid`: The disbursed amount has been fully repaid. * `rejected`: Circle declined the request. * `past_due`: The disbursed amount has not been fully repaid by its due date. ## Credit Express Credit Express is an optional destination configuration for a credit transfer. Instead of disbursing into your Circle Mint wallet, Circle sends the USDC directly onchain to a verified address from your Circle Mint recipient address book. Credit Express is available on both Settlement Advance and Line of Credit. The feature requires that the destination address is already registered and verified through the recipient address book; you cannot register a new address as part of a draw. When a transfer is configured with a Credit Express destination, the disbursement carries an additional onchain delivery status that progresses through `pending`, `initiated`, `complete`, and `failed`. That status is separate from the credit transfer's own status—the transfer reflects the credit relationship between you and Circle, while the destination status reflects the onchain leg of the disbursement. ## Repayment Repayments retire outstanding balances on disbursed transfers. The Credit API supports two repayment paths; product choice determines which paths are available. ### Fiat repayment Fiat repayment is available for both Settlement Advance and Line of Credit. You wire USD to a Circle-issued repayment account; Circle matches the incoming wire to your credit line and applies it against outstanding transfers. The repayment account is initially `unverified` and stays in that state until the first incoming wire is matched to it, at which point it transitions to `active`. The account's wire instructions remain stable across that transition. Each fiat repayment record exposes `repaymentAccountId`, the identifier of the fiat account the repayment was applied to. Pass it to `GET /v1/credit/repaymentAccounts/{fiatAccountId}` to retrieve the account's bank details and wire instructions. The field is present for fiat repayments with a linked account and omitted for crypto repayments. ### Crypto repayment Crypto repayment is available for Line of Credit only. Circle deducts USDC from your Circle Mint wallet and applies it to outstanding transfers. The repayment amount is capped at the current outstanding balance—you cannot prepay beyond what is owed. Settlement Advance does not support crypto repayment; calls to the crypto repayment endpoint against a Settlement Advance credit line return HTTP 400. ## Webhook topics The Credit API publishes three webhook topics so your server can follow credit activity asynchronously rather than polling. Subscribe to whichever topics match the events you need to react to. Payloads mirror the corresponding `GET` endpoints, so a notification carries the same shape your code already handles when fetching a single resource. * `creditTransfers`: Fires when a credit transfer changes status—for example, when a Settlement Advance request is approved and moves to `disbursed`, or when a transfer becomes `past_due`. * `creditFees`: Fires when a fee accrues against the credit line. Cadence matches `feeCadence`, so daily lines emit fee notifications every 24 hours while hourly Line of Credit lines emit them every hour. * `creditRepayments`: Fires when Circle matches an incoming wire repayment or records a completed crypto repayment. The payload includes `repaymentAccountId` for fiat repayments, so you can reconcile the repayment to its bank account without an extra lookup. # Cross-currency exchange Source: https://developers.circle.com/circle-mint/concepts/cross-currency-exchange Understand how the Mint Exchange API exchanges local fiat for USDC and swaps between USDC and EURC through banking partners, the quote-trade-settle model, and delivery-versus-payment settlement. Cross-Currency Exchange is Circle Mint's offchain facility for exchanging local fiat currency into USDC and for swapping between USDC and EURC. The product, exposed through the Mint Exchange API at `/v1/exchange/*`, has been live since 2024 and runs against Circle's regulated banking partners in each supported market. A trade moves through a quote, then a trade record, then a settlement, with funds delivered on a delivery-versus-payment (DvP) basis. This page covers the conceptual model behind the Mint Exchange API, including how it differs from Circle's onchain StableFX product, the lifecycle of a trade, and the scope of supported currency pairs. The Mint Exchange API requires explicit activation on a Circle Mint account. Activation requires Mint customer status, either Circle LLC or Circle SAS as the contracting entity, and local Know Your Customer (KYC) review with Circle's banking partners for Brazilian real (BRL) and Mexican peso (MXN) flows. To enable the API on an account, contact Circle through the [Circle Mint contact form](https://www.circle.com/mint-contact). ## How this product compares to StableFX Two Circle products share the `/v1/exchange/` URL prefix and the "FX" tag, but they are different services with different settlement layers, counterparties, and audiences. The Mint Exchange API is the subject of this page. StableFX is a newer onchain request-for-quote (RFQ) network on Arc. The following table contrasts them. | | Mint Exchange (Cross-Currency) | StableFX | | ---------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | Settlement layer | Offchain delivery-versus-payment through Circle's banking partners and Mint balance. | Onchain payment-versus-payment through a smart-contract escrow on Arc. | | Counterparties | Circle and the Mint customer, with a regulated banking partner per local market. | A network of makers and takers on an institutional RFQ venue. | | URL prefix | `/v1/exchange/*` | `/v1/exchange/stablefx/*` | | Launch status | Live since 2024. | Live on testnet since November 2025; see the StableFX documentation for current mainnet availability. | For onchain FX on Arc, see the [StableFX documentation](/stablefx). The remainder of this page concerns only the Mint Exchange API. ## Quote types Every Mint Exchange trade starts from a quote. Quotes come in two types, set through the `type` field on a quote request. * `reference`: An indicative rate-only quote. Reference quotes do not lock a rate and cannot be accepted to create a trade. They exist to give an application a current view of pricing without committing to a transaction. * `tradable`: A quote with a locked rate. Tradable quotes are valid for **3 seconds** from issuance and are the only quote type that `POST /v1/exchange/trades` will accept. A tradable quote that is not used in its validity window must be replaced by a fresh quote before a trade can be created. ## Quote, trade, and settle The Mint Exchange flow is a three-phase model: a customer first obtains a quote, then creates a trade against that quote, and then settles the trade by either sending fiat to a Circle beneficiary account or letting Circle debit the Mint balance. The following diagram shows the two settlement paths against the same quote-then-trade backbone. ```mermaid theme={null} sequenceDiagram participant C as Mint customer participant M as Mint Exchange API Note over C,M: Quote C->>M: Requests a tradable quote M-->>C: Returns quote with locked rate (valid 3s) Note over C,M: Trade C->>M: Creates a trade against the quote M-->>C: Returns trade in pending state Note over C,M: Settlement (fiat to USDC) C->>M: Sends fiat to Circle beneficiary account with tracking reference M-->>C: Confirms receipt; trade settles into USDC Note over C,M: Settlement (USDC to EURC) M->>M: Debits and credits Mint balance per settlement schedule M-->>C: Settlement batch reflects the completed legs ``` Procedural details, including the API requests for each phase, live in the [Exchange Currencies how-to](/circle-mint/howtos/exchange-currencies). ## Delivery-versus-payment settlement Cross-currency trades settle on a delivery-versus-payment (DvP) basis: Circle receives the inbound leg first and only then delivers the outbound leg. The specific mechanics depend on which currencies are exchanged. **Fiat to or from USDC.** For BRL and MXN pairs, the customer transfers fiat from a registered bank account into Circle's beneficiary account using a Circle-issued tracking reference (an identifier that ties the inbound transfer to the open trade). Once Circle's banking partner confirms receipt, the trade settles and the resulting USDC is credited to the customer's Mint balance. The reverse direction follows the same pattern with the legs reversed. HKD / USDC follows a similar wire-based pattern; consult Circle for current HKD operational details. **USDC to or from EURC.** Both legs are stablecoins held on the Mint balance, so no external fiat transfer is involved. The trade is debited and credited directly against the customer's Mint balance according to a settlement schedule configured offline with Circle. The schedule controls when batches are eligible for settlement and is not exposed as a runtime configuration on the API. ## Settlement batches and settlement instructions A settled trade is grouped into a settlement batch that records the inbound and outbound legs as `payable` and `receivable` details. The batch is the unit of reconciliation: each detail carries its own amount, currency, status, and, for fiat legs, the tracking reference that must accompany the inbound wire or PIX transfer. Settlement instructions are issued per currency and are static, so they can be cached and reused across trades. The shape of the instructions differs by rail. * MXN instructions are returned as wire-style details including an 11-character SWIFT/BIC code, a routing number, and an account number. The inbound transfer must be sent on the SPEI rail. * BRL instructions are returned as PIX details, including ISPB, branch code, account type, and the beneficiary tax ID; the inbound transfer must be sent on PIX. * HKD trades are planned to follow the wire pattern using the CHATS rail; confirm operational specifics with Circle. * USDC and EURC settlement requires no external instructions because the legs settle directly against the Mint balance. ## Trade and settlement lifecycle A trade transitions through a small set of states tracked by the `status` field on the trade record, and the settlement batch it belongs to carries its own status. The following diagram shows the trade lifecycle. ```mermaid theme={null} stateDiagram-v2 [*] --> pending pending --> confirmed pending --> failed confirmed --> pending_settlement confirmed --> failed pending_settlement --> complete pending_settlement --> failed complete --> [*] failed --> [*] ``` The trade states have the following meanings: * `pending`: The trade has been created against an accepted quote but is not yet executed. Funds should not be sent in this state. * `confirmed`: The trade is fully executed and ready for the customer to send the inbound leg, when one is required. * `pending_settlement`: The trade is grouped into a settlement batch and is waiting for the batch to settle. * `complete`: The trade has settled and the outbound leg has been delivered. * `failed`: The trade did not complete and no settlement occurs. A settlement batch reports its own progress through the `pending` and `settled` values of `SettlementStatus`, and each detail in a batch reports a `pending` or `completed` value of its own. ## Daily limits Circle enforces per-currency daily limits on Mint Exchange activity as an operational guard rail. The current limit, the used amount, and the remaining amount for each supported currency are exposed through `GET /v1/exchange/fxConfigs/dailyLimits`. Production planning should account for these caps, especially for high-throughput corridors, because trades that would exceed the daily available amount are rejected. ## Supported currency pairs The Mint Exchange API covers a defined scope of currency pairs, each tied to a contracting entity and, for local fiat corridors, a local KYC review. The following table summarizes the pairs supported end-to-end today. | Currency pair | Entity required | Additional eligibility | | ------------- | ------------------------ | ------------------------------------------------------------------ | | BRL / USDC | Circle LLC | KYC with Circle's banking partner in Brazil; PIX settlement rail. | | MXN / USDC | Circle LLC | KYC with Circle's banking partner in Mexico; SPEI settlement rail. | | EURC / USDC | Circle LLC or Circle SAS | Settles against the Mint balance per the configured schedule. | | HKD / USDC | Circle LLC | CHATS settlement rail. | The `ExchangeRateOptionalAmountMoney` enum in the OpenAPI specification lists additional currency codes (AED, GBP, CNH, SGD), but those codes are not supported end-to-end at this time. Daily limits and settlement instructions are only published for the pairs in the preceding table. For the procedural walkthrough of obtaining a quote, creating a trade, and sending funds, see the [Exchange Currencies how-to](/circle-mint/howtos/exchange-currencies). For the broader Mint product, see the [Circle Mint overview](/circle-mint). # 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. # Institutional API Source: https://developers.circle.com/circle-mint/concepts/institutional-api Understand how the Institutional API lets Circle Mint distributors mint, redeem, and transfer on behalf of compliance-screened external entities with their own per-entity wallets. The Institutional API is for Circle Mint customers with the institutional entitlement (Distributors) who operate fiat-to-stablecoin flows on behalf of their own institutional end clients. It lets you onboard those end clients as external entities, screen them through Circle compliance, and then mint, redeem, and transfer USDC on their behalf. Each entity gets its own dedicated wallet so balances and money movement stay segregated by counterparty. ## Why use the Institutional API Without the Institutional API, every counterparty that needs to mint or redeem through Circle would have to onboard as a direct Circle Mint customer. The Institutional API solves that by letting one Distributor's Mint account act as the integration surface for many institutional end clients. Circle still runs compliance on each external entity, but the Distributor—not the end client— holds the API key and runs the integration. The API is intended for B2B2B Distributors servicing institutional end clients, such as: * Commercial banks * Institutional banks * Centralized exchanges * Custodians * TradFi trading platforms * Real-world asset (RWA) platforms Retail-only use cases and internal-only flows do not require the Institutional API. ## Distributor and external entity The Institutional API revolves around two roles. The Distributor is the Circle Mint customer; the external entity is the Distributor's institutional end client. Each role has a different relationship to Circle and to the API. | Aspect | Distributor | External entity | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Role | The Circle Mint customer integrating the Institutional API. | The Distributor's institutional end client. | | API access | Holds the API key and authenticates all requests. | Never accesses the Circle Mint API directly. | | Wallet | Owns a primary wallet identified by `masterWalletId`, used as the default source and destination when no entity wallet is specified. | Receives a dedicated subaccount wallet (type `end_user_wallet`) after Circle accepts the entity in compliance. | | Identity | Calls [`POST /v1/externalEntities`](/api-reference/circle-mint/institutional/create-external-entity) to onboard each external entity. | Identified by `businessName`, `businessUniqueIdentifier` (tax ID), `identifierIssuingCountryCode`, and `address` submitted by the Distributor. | | Money movement | Initiates mints, redemptions, and onchain transfers on behalf of each accepted entity. | Funds flow through the entity's `walletId`; the entity itself does not call Circle APIs. | ## Compliance lifecycle Every call to [`POST /v1/externalEntities`](/api-reference/circle-mint/institutional/create-external-entity) triggers a synchronous sanctions screening on the entity's business name, tax ID (`businessUniqueIdentifier`), issuing country code, and address. The 201 response returns the entity in `complianceState: PENDING` along with the provisioned `walletId`. The final decision—`ACCEPTED` or `REJECTED`—is delivered asynchronously through the `externalEntities` webhook topic. ```mermaid theme={null} sequenceDiagram participant D as Distributor participant C as Circle participant W as Distributor webhook endpoint D->>C: POST /v1/externalEntities C-->>D: 201 { complianceState: "PENDING" } Note over C: Synchronous sanctions screening alt Accepted C-->>W: externalEntities { complianceState: "ACCEPTED", walletId } else Rejected C-->>W: externalEntities { complianceState: "REJECTED" } end ``` The `walletId` is returned at creation but is unusable while the entity remains in `PENDING` or `REJECTED`. Wait for the `externalEntities` webhook to deliver `complianceState: ACCEPTED` before referencing the entity's wallet in any other endpoint. ## Per-entity wallet model When Circle accepts an external entity, it provisions a dedicated wallet of type `end_user_wallet` and surfaces the wallet identifier as `walletId` (for example, `"212000"`). The wallet is owned by the Distributor's Mint account but segregated to that entity, so balances and money movement remain attributable per counterparty. Every relevant Mint endpoint accepts the entity's `walletId`: * On `GET` endpoints (such as listing deposits, payouts, or transfers), pass the entity wallet as the `walletId` query parameter: `?walletId=212000`. * On `POST` endpoints (such as creating a payout or an onchain transfer), pass the entity wallet in the request body as `"source": { "type": "wallet", "id": "212000" }`. If `walletId` is omitted on a request, Circle defaults to the Distributor's primary wallet (`masterWalletId`). The primary wallet remains available for the Distributor's own balances and money movement; the entity wallets sit alongside it as siblings, not as children of the primary wallet. ## Operational flows The Institutional API supports three flows on behalf of an accepted external entity: minting, redeeming, and onchain transfer. The table below summarizes each flow at a conceptual level. For endpoint-level steps, see [Manage institutional subaccounts](/circle-mint/quickstarts/manage-institutional-subaccounts). | Flow | What it does | Where the entity wallet appears | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Mint on behalf of an entity | The end client wires fiat against entity-scoped wire instructions; Circle credits the entity wallet and fires a `deposits` webhook. | `walletId` query parameter on the wire-instructions request. | | Redeem on behalf of an entity | The Distributor submits a payout from the entity wallet. The Institutional Direct fee is deducted at redemption, so `toAmount` reflects the net amount the entity's bank receives. | `source.id` in the payout request body. | | Onchain transfer on behalf of an entity | Outbound transfers draw from the entity wallet and target a verified destination from the Distributor's address book; inbound transfers use a per-entity deposit address. | `walletId` query parameter on deposit-address requests; `source.id` in the transfer request body. | ## Billing Institutional Direct is the default billing model for the Institutional API. A gross flat fee is applied to every redemption, charged to the end client at the point of redemption and deducted from the `toAmount` returned on the payout response. The Distributor sees the net amount in the payout payload, and the entity's bank receives that net amount. Alternative billing models are available by exception. Contact Circle to arrange one outside the API. ## What is not supported The Institutional API does not support the following operations on behalf of an external entity: * Editing or deleting an external entity once it is created. Contact Circle to make changes. * Express routes from entity subaccount wallets. * Local-currency onramp flows (such as BRL, MXN, or EURC swaps) on behalf of entity subaccounts. * Stablecoin Payins and Stablecoin Payouts against entity wallets. * Cross-Currency Exchange against entity wallets. * Credit API operations on behalf of an external entity. ## See also * [Manage institutional subaccounts](/circle-mint/quickstarts/manage-institutional-subaccounts) * [Webhook notifications: `externalEntities`](/circle-mint/references/webhook-notifications#externalentities) # 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: Exchange currencies Source: https://developers.circle.com/circle-mint/howtos/exchange-currencies Use the Mint Exchange API to exchange local fiat for USDC or swap between USDC and EURC through a quote, trade, and settlement flow. Use the Mint Exchange API to exchange a supported local fiat currency for USDC or to swap between USDC and EURC. This guide walks the full quote, trade, and settle flow for every supported pair as variants of the same procedure. For the conceptual model behind quotes, trades, settlement batches, and delivery-versus-payment settlement, see [Cross-currency exchange](/circle-mint/concepts/cross-currency-exchange). ## Prerequisites Before you begin, make sure that you've: * Contacted your Circle representative to activate Cross-Currency Exchange on your Circle Mint account. * Linked a bank account to Circle Mint for the local fiat side of the trade (not required for USDC and EURC swaps). * Configured API authentication per the [Getting Started quickstart](/circle-mint/quickstarts/getting-started). The examples use a `$API_KEY` environment variable for the bearer token and the sandbox base URL, `https://api-sandbox.circle.com`. ## Step 1. Register a fiat trading account For BRL or MXN pairs, register the linked fiat account as the settlement account for that currency by sending a `PUT` request to [`/v1/exchange/fxConfigs/accounts`](/api-reference/circle-mint/cross-currency/create-fx-account). This is a one-time setup per currency. Skip this step for USDC and EURC swaps and for HKD trades whose fiat account is configured offline. ```bash theme={null} curl -X PUT https://api-sandbox.circle.com/v1/exchange/fxConfigs/accounts \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016", "currency": "MXN" }' ``` Expected response: ```json theme={null} { "data": { "currency": "MXN", "fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016", "createDate": "2026-04-10T17:50:09.452Z", "updateDate": "2026-04-10T17:50:09.452Z" } } ``` To register a Brazilian real account instead, set `currency` to `BRL` and use the PIX bank account `id` returned by your account setup. ## Step 2. Check the daily limit Verify that the trade you plan to create has enough daily headroom by calling [`GET /v1/exchange/fxConfigs/dailyLimits`](/api-reference/circle-mint/cross-currency/get-daily-fx-limits). The endpoint returns the per-currency limit, the amount used so far in the current day, and the amount still available. Trades that would exceed the available amount are rejected at submission. ```bash theme={null} curl https://api-sandbox.circle.com/v1/exchange/fxConfigs/dailyLimits \ -H "Authorization: Bearer $API_KEY" ``` Expected response: ```json theme={null} { "data": { "dailyLimits": { "EURC": { "limit": "1000000.00", "usage": "0.00", "available": "1000000.00" }, "MXN": { "limit": "1000000.00", "usage": "0.00", "available": "1000000.00" }, "USDC": { "limit": "1000000.00", "usage": "0.00", "available": "1000000.00" }, "BRL": { "limit": "1000000.00", "usage": "0.00", "available": "1000000.00" } } } } ``` ## Step 3. Request a tradable quote Send a `POST` request to [`/v1/exchange/quotes`](/api-reference/circle-mint/cross-currency/get-quote) with `type` set to `tradable` to lock a rate for 3 seconds. Reference quotes are indicative only and cannot be accepted in Step 4; see the [quote types section](/circle-mint/concepts/cross-currency-exchange#quote-types) for the distinction. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/exchange/quotes \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "tradable", "idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b", "from": { "currency": "MXN", "amount": "100.00" }, "to": { "currency": "USDC" } }' ``` Expected response: ```json theme={null} { "data": { "id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1", "rate": 0.0597, "from": { "currency": "MXN", "amount": "100.00" }, "to": { "currency": "USDC", "amount": "5.97" }, "expiry": "2026-04-10T14:37:23.804Z", "type": "tradable", "estimatedSettlementTime": "2026-04-10T17:37:23.804Z" } } ``` To request a quote for a USDC and EURC swap, use the same endpoint with `from` and `to` set to `USDC` and `EURC` (in either direction). The request requires the same `idempotencyKey` and `tradable` quote type. ## Step 4. Accept the quote by creating a trade Lock the quoted rate by sending a `POST` request to [`/v1/exchange/trades`](/api-reference/circle-mint/cross-currency/create-fx-trade) with the `quoteId` from Step 3 and a new `idempotencyKey`. Only `tradable` quotes are accepted; submitting a `reference` quote returns an error. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/exchange/trades \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" }' ``` Expected response: ```json theme={null} { "data": { "id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "from": { "currency": "MXN", "amount": "100.00" }, "to": { "currency": "USDC", "amount": "5.97" }, "status": "pending", "createDate": "2026-04-10T14:37:20.804Z", "updateDate": "2026-04-10T14:37:20.804Z", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1" } } ``` Record the trade `id`. You use it in Step 6 to poll the trade status. ## Step 5. Retrieve settlement instructions and send funds This step applies to fiat pairs. For USDC and EURC swaps, no inbound transfer is required; see the note at the end of this step. Fetch the per-currency beneficiary details by sending a `GET` request to [`/v1/exchange/trades/settlements/instructions/{currency}`](/api-reference/circle-mint/cross-currency/get-settlement-instructions). The endpoint accepts `BRL` and `MXN` as path parameters. Settlement instructions are static for a given currency and can be cached and reused across trades. ```bash theme={null} curl https://api-sandbox.circle.com/v1/exchange/trades/settlements/instructions/MXN \ -H "Authorization: Bearer $API_KEY" ``` For MXN, the response returns wire-style instructions. Send the inbound peso transfer on the SPEI rail using these details and include the `trackingRef` in the wire reference field so Circle can match the inbound transfer to the trade. ```json theme={null} { "data": { "currency": "MXN", "fiatAccountType": "wire", "instruction": { "beneficiary": { "name": "CIRCLE INTERNET FINANCIAL INC", "address1": "99 HIGH STREET", "address2": "BOSTON MA 02110" }, "beneficiaryBank": { "name": "CIRCLE BANKING PARTNER", "swiftCode": "BANKMXMMXXX", "routingNumber": "322286803", "accountNumber": "127180987654321012", "currency": "MXN", "address": "AVENIDA INSURGENTES SUR 3579", "postalCode": "14020", "country": "MX" }, "trackingRef": "ezBrwN2nP5Bz18Lu" } } } ``` For BRL, the response returns PIX instructions. After the trade reaches settlement, call [`GET /v1/exchange/trades/settlements`](/api-reference/circle-mint/cross-currency/get-settlements) and read `reference` from the payable entry in `details[]`. Send the inbound transfer on the PIX rail using that value as the payment reference and the `accountNumber` from the instructions as the destination. ```json theme={null} { "data": { "currency": "BRL", "fiatAccountType": "pix", "instruction": { "ispb": "87654321", "branchCode": "0001", "accountNumber": "12345678", "name": "Circle Internet Financial LLC", "accountType": "checking", "taxId": "12.345.678/0001-90", "bankName": "Banco Example S.A.", "compeCode": "000" } } } ``` For HKD↔USDC trades, settlement instructions are exchanged out of band and follow a similar wire-based pattern using the CHATS rail. Confirm the inbound transfer details with Circle before sending HKD. In the sandbox, simulate the inbound BRL leg with [`POST /v1/mocks/payments/pix`](/api-reference/circle-mint/cross-currency/create-mock-pix-payment). After the trade reaches settlement: 1. Call [`GET /v1/exchange/trades/settlements`](/api-reference/circle-mint/cross-currency/get-settlements). 2. Read `reference` from the payable entry in `details[]`, for example `FXR3T6YSTY`. 3. Use that value as `trackingRef` in the mock request, along with the `accountNumber` from the settlement instructions. Note that `GET /v1/exchange/trades/settlements/instructions/BRL` does not include the per-trade reference in sandbox; it only appears in the settlements batch returned by `GET /v1/exchange/trades/settlements`. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/mocks/payments/pix \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "trackingRef": "FXR3T6YSTY", "amount": { "amount": "100.00", "currency": "BRL" }, "accountNumber": "12345678" }' ``` Expected response: ```json theme={null} { "data": { "trackingRef": "FXR3T6YSTY", "amount": { "amount": "100.00", "currency": "BRL" }, "beneficiaryAccountNumber": "12345678", "status": "pending" } } ``` USDC and EURC swaps do not require an inbound transfer. Both legs are debited and credited against the Mint balance per the settlement schedule configured offline with Circle. See [delivery-versus-payment settlement](/circle-mint/concepts/cross-currency-exchange#delivery-versus-payment-settlement) for details. ## Step 6. Poll the trade status Send a `GET` request to [`/v1/exchange/trades/{id}`](/api-reference/circle-mint/cross-currency/get-fx-trade-id) to track the trade as it moves toward settlement. The trade transitions through `pending`, `confirmed`, `pending_settlement`, and `complete` (or `failed`) as it progresses. See the [trade and settlement lifecycle](/circle-mint/concepts/cross-currency-exchange#trade-and-settlement-lifecycle) for the full state machine. ```bash theme={null} curl https://api-sandbox.circle.com/v1/exchange/trades/7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6 \ -H "Authorization: Bearer $API_KEY" ``` Expected response: ```json theme={null} { "data": { "id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6", "from": { "currency": "MXN", "amount": "100.00" }, "to": { "currency": "USDC", "amount": "5.97" }, "status": "complete", "createDate": "2026-04-10T14:37:20.804Z", "updateDate": "2026-04-10T14:55:00.000Z", "quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1", "settlementId": "67276b7d-7ea5-4f22-a231-09e2d2891c36", "rate": "0.0597", "expectedBatchTime": "2026-04-10T15:00:00.000Z", "estimatedSettlementTime": "2026-04-10T17:00:00.000Z" } } ``` ## Step 7. Retrieve the settlement batch Once the trade reaches `complete`, retrieve the settlement batch that holds the trade's legs by sending a `GET` request to [`/v1/exchange/trades/settlements`](/api-reference/circle-mint/cross-currency/get-settlements). Use the `type` query parameter to filter for the inbound (`account_receivable`) or outbound (`account_payable`) side. Each batch lists its legs in the `details` array, marked as `payable` and `receivable`. ```bash theme={null} curl "https://api-sandbox.circle.com/v1/exchange/trades/settlements?type=account_receivable" \ -H "Authorization: Bearer $API_KEY" ``` Expected response: ```json theme={null} { "data": [ { "id": "67276b7d-7ea5-4f22-a231-09e2d2891c36", "entityId": "c5692eb6-33f9-431d-9481-2eee38f02081", "status": "settled", "createDate": "2026-04-10T14:45:04.729Z", "updateDate": "2026-04-10T14:55:00.000Z", "details": [ { "id": "02bd22dc-b40f-49b8-b2d8-6e69f82cfca0", "type": "payable", "status": "completed", "reference": "ezBrwN2nP5Bz18Lu", "amount": { "currency": "MXN", "amount": "100.00" }, "createDate": "2026-04-10T14:45:04.728Z", "updateDate": "2026-04-10T14:55:00.000Z" }, { "id": "afbd8d53-34ea-42fa-9691-4a5c0dd96c53", "type": "receivable", "status": "completed", "amount": { "currency": "USDC", "amount": "5.97" }, "createDate": "2026-04-10T14:45:04.728Z", "updateDate": "2026-04-10T14:55:00.000Z" } ] } ] } ``` Repeat the request with `type=account_payable` to retrieve the outbound leg. * [Getting Started](/circle-mint/quickstarts/getting-started): Authenticate to the Mint API and set up an API key. # 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. Third-party payouts may require Travel Rule compliance data; see [Travel rule compliance](/circle-mint/references/travel-rule-compliance) for thresholds, schemas, and failure modes. ## 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" } } ``` ## See also * [How minting and redemption works](/circle-mint/concepts/how-minting-works): understand the transfer lifecycle * [Travel rule compliance](/circle-mint/references/travel-rule-compliance): thresholds, schemas, and failure modes for third-party payouts * [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 # How mTLS authentication works Source: https://developers.circle.com/circle-mint/mtls-authentication Learn how mutual TLS authentication adds a transport-layer certificate check to Circle Mint API access, as a MiCA requirement or an optional security enhancement. Mutual Transport Layer Security (mTLS) adds transport-layer certificate authentication on top of your existing API key. With mTLS enabled, both Circle and your client verify each other's identity before any API request is processed. A request is accepted only when it arrives over a connection that both sides have authenticated. ## Who should use mTLS You can enable mTLS in either of two situations: * **As a MiCA requirement.** Entities operating in an EU/EEA member state under the Markets in Crypto-Assets (MiCA) regulation must use mTLS on regulated API endpoints. * **As an optional security enhancement.** Any Circle Mint customer with an active API key can opt in to mTLS for an additional layer of transport-level authentication. Opting in is voluntary, and there's no regulatory prerequisite. The certificate model, setup flow, and API key lifecycle are the same in both cases. Only two things differ, depending on why you enable mTLS: | Aspect | MiCA-regulated | Optional (non-MiCA) | | ------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------- | | API hostname | Regional EU hostname `api-eu.circle.com` | Standard hostname `api.circle.com` (unchanged) | | Server certificate you validate | A Qualified Website Authentication Certificate (QWAC), validated for PSD2 | Circle's standard server certificate, validated as usual | ## What is mTLS Standard TLS (one-way TLS) authenticates only the server. Your client verifies Circle's certificate during the TLS handshake, but Circle has no way to verify your client's identity at the transport layer. Mutual TLS (mTLS) extends this model so that both sides present certificates: * **Server authentication**: Circle presents its certificate to your client, just as in standard TLS. * **Client authentication**: Your client presents a client certificate issued by Circle's private certificate authority (CA) during the same handshake. Circle validates that certificate against its private CA before accepting the connection. The result is a two-way trust relationship established before any HTTP traffic flows. Only after both certificates are validated does the connection proceed to the application layer, where your API key is checked as usual. ```mermaid theme={null} sequenceDiagram participant C as Your client participant S as Circle API Note over C,S: TLS handshake begins S->>C: Presents server certificate C->>C: Validates server certificate C->>S: Presents client certificate (issued by Circle's private CA) S->>S: Validates client certificate against its private CA Note over C,S: TLS handshake complete Note over C,S: Application layer C->>S: Sends API request with Bearer token S->>S: Validates API key S-->>C: Returns API response ``` ## Client certificates Circle issues your client certificate from its private certificate authority (CA), managed through AWS Private CA. You do not purchase a Qualified Website Authentication Certificate (QWAC) or any other certificate from a third party. The issuance flow keeps your private key in your own environment: You generate an Elliptic Curve Digital Signature Algorithm (ECDSA) P-256 key pair and a PKCS#10 certificate signing request (CSR) locally. Circle accepts only ECDSA P-256 keys; RSA keys and other curves are rejected. You submit the CSR, which contains only your public key, to Circle. Your private key never leaves your environment. Circle validates the CSR, issues a signed client certificate from its private CA, and returns a single file, `client-fullchain.pem`, through a secure, out-of-band channel. It contains your signed certificate followed by the CA certificate chain. Circle assigns the certificate fields, including the subject name, key usage (`digitalSignature`), and extended key usage (`clientAuth`). The certificate is valid for 365 days from the date of issuance. ### Certificate validation Because Circle issues your client certificate from its own private CA, Circle validates the certificate you present against that CA on every request. You do not pre-register the certificate separately; presenting it during the TLS handshake is sufficient. If the certificate is expired or does not chain to Circle's private CA, requests fail at the transport layer before the API key is evaluated. For step-by-step instructions on generating a key pair and CSR and obtaining your certificate, see [How-to: Set up mTLS authentication](/circle-mint/set-up-mtls-authentication). ### Server certificate verification Circle also presents its own server certificate during the handshake. How you validate it depends on why you enabled mTLS. **MiCA-regulated**: Circle presents a QWAC at `api-eu.circle.com`. Validate it in one of two ways: * Have a payment aggregator validate the certificate on your behalf. * Validate it directly by verifying the certificate chain against the EU Trusted Lists, checking OCSP revocation status, and confirming the PSD2 QcStatements in the certificate. **Optional (non-MiCA)**: Circle presents its standard server certificate at `api.circle.com`. Validate it the same way you validate any HTTPS connection, using the CA certificate chain contained in `client-fullchain.pem`. No QWAC or EU Trusted List steps apply. ## How mTLS layers on top of API key authentication mTLS does not replace API key authentication. It adds a second authentication layer at the transport level. Every request to a protected endpoint must pass both checks. | Layer | Standard auth | mTLS-enabled auth | | ------------------ | ------------------------------------------------------------- | --------------------------------------------------------- | | Transport (TLS) | One-way TLS: your client verifies Circle's server certificate | Mutual TLS: both sides exchange and validate certificates | | Application (HTTP) | API key in `Authorization: Bearer` header | API key in `Authorization: Bearer` header (unchanged) | | Total factors | 1 (API key) | 2 (client certificate + API key) | A request that presents a valid client certificate but an invalid API key is rejected at the application layer. A request with a valid API key but no client certificate (or an invalid one) is rejected at the transport layer before the API key is ever evaluated. ## Scope of mTLS When mTLS is enabled on your entity, every Circle Mint API endpoint requires a valid client certificate. mTLS applies globally to your entity's API traffic; there is no per-endpoint configuration or opt-in. Any request that omits a valid client certificate is rejected at the transport layer, regardless of which endpoint it targets. ### API hostname The hostname you call depends on why you enabled mTLS: * **MiCA-regulated**: Use the regional EU hostname `api-eu.circle.com` for all API traffic. For example, call `https://api-eu.circle.com/v1/businessAccount/balances` rather than `https://api.circle.com/v1/businessAccount/balances`. Requests sent to `api.circle.com` are rejected. * **Optional (non-MiCA)**: Continue to use the standard hostname `api.circle.com`. Enabling mTLS does not change your endpoint URLs. ## API key lifecycle under mTLS Enabling mTLS on your entity triggers significant changes to your API key management. Understanding these changes is critical before you begin the setup process. ### Key revocation when mTLS is enabled When Circle enables mTLS on your entity, all existing API keys are immediately revoked. You must generate new API keys through the Mint Console with multi-factor authentication (MFA) before you can make API calls. Plan your migration carefully. Enabling mTLS revokes every existing API key on the entity. Any integration using an old key stops working immediately. ### Mandatory 180-day key lifetime API keys on mTLS-enabled entities carry a maximum lifetime of 180 days. Circle enforces this limit automatically. After 180 days, an API key expires and can no longer authenticate requests. This limit applies to every mTLS-enabled entity, whether you enabled mTLS optionally or under MiCA. To avoid service interruptions, generate a replacement key before the current key expires and rotate your integration to use the new key. For detailed rotation procedures, see [How-to: Rotate an mTLS API key](/circle-mint/rotate-mtls-api-key). ### Summary of key lifecycle changes | Aspect | Standard API keys | mTLS-enabled API keys | | ------------------------------- | ----------------- | ------------------------------------- | | Generation | Mint Console | Mint Console with MFA required | | Maximum lifetime | No enforced limit | 180 days | | Revocation when mTLS is enabled | N/A | All existing keys revoked immediately | | Authentication layers | API key only | Client certificate + API key | For step-by-step instructions on enabling mTLS and configuring your client certificate, see [How-to: Set up mTLS authentication](/circle-mint/set-up-mtls-authentication). # 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. You can also harden API access with a transport-layer certificate check. You can add mutual TLS (mTLS) on top of API key authentication for an extra layer of transport-level security. Opting in is optional for most customers and required for entities operating under the EU Markets in Crypto-Assets (MiCA) regulation. mTLS changes how you generate and rotate API keys. See [How mTLS authentication works](/circle-mint/mtls-authentication). ## 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. ## Use the TypeScript SDK (optional) Circle publishes a TypeScript SDK that wraps the Circle Mint API. Install it from the npm registry: ```bash theme={null} npm install @circle-fin/circle-sdk ``` See the [`circle-nodejs-sdk` repository](https://github.com/circlefin/circle-nodejs-sdk) for usage examples, type definitions, and instructions for filing issues or contributing. # Quickstart: Draw and repay a line of credit Source: https://developers.circle.com/circle-mint/quickstarts/line-of-credit Check your credit line, request an auto-disbursed draw, observe fees, and repay with crypto or wire to close out a Line of Credit transfer. This quickstart walks through a complete Line of Credit draw lifecycle: you check the credit line, request a draw that auto-disburses, observe accrued fees, repay using USDC from your Circle Mint wallet (or by wire as a fallback), and confirm the transfer closes out. For the conceptual model behind these calls, see the [Credit API](/circle-mint/concepts/credit-api) concept page. ## Prerequisites Before you begin, make sure that you've: * Contacted your Circle representative to activate Line of Credit on your Circle Mint account. * Confirmed your Circle Mint wallet holds the required USDC `minBalance` for your credit line. * Configured API authentication per [Getting Started](/circle-mint/quickstarts/getting-started). Examples below use `$API_KEY` and the base URL `https://api-sandbox.circle.com`. * Identified the `fiatAccountId` of a wire bank account if you intend to use wire repayment as a fallback in Step 2. ## Step 1. Check the credit line Call `GET /v1/credit` to confirm your credit line is active and has sufficient capacity for the draw. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit \ -H "Authorization: Bearer $API_KEY" ``` ```json 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": "0.00", "currency": "USD" }, "minBalance": { "amount": "100000.00", "currency": "USD" }, "validationErrors": [], "createDate": "2024-01-15T10:30:00.000Z", "updateDate": "2024-03-20T14:22:00.000Z" } } ``` Before proceeding, confirm: * `product` is `lineOfCredit`. * `available.amount` covers the draw amount you plan to request. * `validationErrors` is an empty array. If `validationErrors` is non-empty, resolve each entry before requesting a draw: * `INSUFFICIENT_BALANCE`: top up your Circle Mint USDC wallet to at least the credit line's `minBalance`. * `PENDING_FEES`: wait for pending fees to settle, or repay them with `POST /v1/credit/cryptoRepayment`. * `OVERDUE_TRANSFERS`: repay overdue transfers by wire or crypto repayment to clear the block. ## Step 2. Get wire repayment instructions (optional fallback) Line of Credit supports crypto repayment directly from your Circle Mint wallet, so wire instructions are only needed if you plan to repay by wire. To retrieve them, call `GET /v1/credit/repaymentAccounts/{fiatAccountId}` with the ID of the wire bank account you'll send from. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/repaymentAccounts/b8627ae8-732b-4d25-b947-1df8f4007a29 \ -H "Authorization: Bearer $API_KEY" ``` ```json 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": "CRYPTO99", "routingNumber": "999999999", "accountNumber": "3302726104", "currency": "USD", "name": "CIRCLE BANKING PARTNER", "address": "100 MAIN STREET", "city": "NEW YORK", "postalCode": "10001", "country": "US" } } } } ``` The repayment account is `unverified` until Circle matches the first incoming wire repayment to it, after which it transitions to `active`. In the sandbox, simulate a matching wire and verify the account by posting a mock repayment. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/mocks/repayments \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amount": { "amount": "50000.00", "currency": "USD" } }' ``` ```json theme={null} { "data": { "trackingRef": "CIR3XBZZ4N", "amount": { "amount": "50000.00", "currency": "USD" }, "status": "pending" } } ``` ## Step 3. Request a draw Call `POST /v1/credit/transfers` with an idempotency key and the draw amount. To use Credit Express, include the optional `destination` field to disburse directly to a verified address from your Circle Mint recipient address book; omit it to land the disbursement in your Mint wallet. Line of Credit draws auto-disburse—no manual review is required. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/transfers \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "amount": { "amount": "50000.00", "currency": "USD" }, "destination": { "type": "verified_blockchain", "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } }' ``` ```json 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—it identifies this draw for the remaining steps. The transfer starts in `requested` and transitions automatically to `disbursed` once funds land in your Mint wallet, or onchain at the verified address if you supplied a `destination`. For details on the blockchain destination, see [Credit Express](/circle-mint/concepts/credit-api#credit-express). ## Step 4. Observe disbursement and fee accrual Once the transfer is `disbursed`, `recurringFee` begins accruing against the outstanding balance at the credit line's cadence. ### 4.1. Poll the transfer Call `GET /v1/credit/transfers/{id}` to inspect the disbursement, due date, and accrued fees. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/transfers/a1c2e3f4-5678-4d90-b123-456789abcdef \ -H "Authorization: Bearer $API_KEY" ``` ```json 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 is the principal plus accrued fees. For hourly cadence, fees accrue every hour and the `dueDate` is 24 hours after `disbursedDate`; for daily cadence, fees accrue every 24 hours and the `dueDate` is 7 days after `disbursedDate`. See [fee cadence and repayment timing](/circle-mint/concepts/credit-api#fee-cadence-and-repayment-timing) for the underlying model. If you supplied a Credit Express `destination`, the `blockchainDestination` block tracks the onchain leg separately from the credit transfer. The `blockchainDestination.transferId` references the underlying Circle Mint transfer once disbursement initiates onchain. ### 4.2. Subscribe to webhooks For asynchronous updates, subscribe to `creditTransfers`, `creditFees`, and `creditRepayments` in the Circle Mint Console. Webhook payloads mirror the corresponding `GET` endpoints. See [webhook topics](/circle-mint/concepts/credit-api#webhook-topics) for what each topic publishes. ## Step 5. Repay with crypto (the fast path) Call `POST /v1/credit/cryptoRepayment` with the amount you want to apply against the outstanding balance. The endpoint deducts USDC from your Circle Mint wallet at the time of the call. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/cryptoRepayment \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "d7a3e2c1-8f45-4b91-ae67-2c9d0f8b3e5a", "amount": { "amount": "50150.00", "currency": "USD" } }' ``` ```json theme={null} { "data": { "id": "e5f6a7b8-9012-3456-efab-345678901234", "amount": { "amount": "50150.00", "currency": "USD" }, "status": "pending", "createDate": "2024-03-25T10:00:00.000Z", "updateDate": "2024-03-25T10:00:00.000Z" } } ``` Repayment constraints to keep in mind: * The requested amount is capped at the outstanding balance across the credit line. A request that exceeds the cap returns HTTP 400. * Crypto repayment is Line of Credit only. Settlement Advance does not support this endpoint—see the [Settlement Advance quickstart](/circle-mint/quickstarts/settlement-advance) for SA repayment. ## Step 6. Repay with a wire (alternative) If you prefer fiat repayment, wire USD to Circle using the `wireInstructions` from Step 2. The `trackingRef` on the wire lets Circle match the payment to your credit line. In the sandbox, simulate the wire by posting a mock repayment: ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/mocks/repayments \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amount": { "amount": "50150.00", "currency": "USD" } }' ``` ```json theme={null} { "data": { "trackingRef": "CIR3XBZZ4N", "amount": { "amount": "50150.00", "currency": "USD" }, "status": "pending" } } ``` ## Step 7. Confirm the repayment List repayments filtered to your transfer by calling `GET /v1/credit/repayments?transferId={id}` and confirm a record exists with `status: completed`. The `type` is `crypto` for repayments from Step 5 or `fiat` for wire repayments from Step 6. Wire repayments initially return `status: pending` and transition to `completed` once Circle matches the inbound wire, so you may need to poll until settlement; the example below shows the final settled state. ```bash theme={null} curl "https://api-sandbox.circle.com/v1/credit/repayments?transferId=a1c2e3f4-5678-4d90-b123-456789abcdef" \ -H "Authorization: Bearer $API_KEY" ``` ```json theme={null} { "data": [ { "id": "d4e5f6a7-b890-1234-defa-234567890123", "transferId": "a1c2e3f4-5678-4d90-b123-456789abcdef", "amountApplied": { "amount": "50150.00", "currency": "USD" }, "paymentAmount": { "amount": "50150.00", "currency": "USD" }, "type": "crypto", "status": "completed", "settlementDate": "2024-03-25T10:00:00.000Z", "createDate": "2024-03-25T10:00:00.000Z", "updateDate": "2024-03-25T10:00:00.000Z" } ] } ``` `paymentAmount` is the total repayment received and `amountApplied` is the portion applied to this transfer's outstanding balance (principal plus fees). Crypto repayments omit `repaymentAccountId`; fiat repayments include it, set to the fiat account the wire was matched to. Then call `GET /v1/credit/transfers/{id}` again and confirm the transfer is `paid`. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/transfers/a1c2e3f4-5678-4d90-b123-456789abcdef \ -H "Authorization: Bearer $API_KEY" ``` ```json theme={null} { "data": { "id": "a1c2e3f4-5678-4d90-b123-456789abcdef", "amount": { "amount": "50000.00", "currency": "USD" }, "status": "paid", "outstanding": { "amount": "0.00", "currency": "USD" }, "fees": { "total": { "amount": "150.00", "currency": "USD" }, "unpaid": { "amount": "0.00", "currency": "USD" } }, "dueDate": "2024-03-27T14:22:00.000Z", "paidDate": "2024-03-25T10:00:00.000Z", "disbursedDate": "2024-03-20T14:22:00.000Z", "createDate": "2024-03-20T14:20:00.000Z", "updateDate": "2024-03-25T10:00:00.000Z" } } ``` The transfer now shows `status: paid`, `outstanding` is zero, and `fees.unpaid` is zero—the draw lifecycle is complete. # Manage institutional subaccounts Source: https://developers.circle.com/circle-mint/quickstarts/manage-institutional-subaccounts Step-by-step guide for Circle Mint distributors to create external entities, mint and redeem on their behalf, transfer USDC onchain, and query entity-scoped activity. Operate fiat-to-stablecoin flows on behalf of an external entity end-to-end: onboard the entity, wait for the compliance decision, then mint, redeem, and transfer USDC onchain on its behalf using the entity's dedicated `walletId`. Use this guide when you hold the Institutional API entitlement as a Distributor and you're integrating a new institutional counterparty into your Circle Mint account. For the conceptual model, see [Institutional API](/circle-mint/concepts/institutional-api). ## Prerequisites Before you begin, ensure that you've: * Confirmed the Institutional API entitlement is enabled on your Circle Mint account. [Contact Circle](https://www.circle.com/mint-contact) if you don't see it. * Created an API key with institutional permissions. * Set up a webhook subscription that includes the `externalEntities`, `deposits`, `transfers`, and `payouts` topics. See [Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications). * Verified at least one linked bank account for receiving wires from your end client. See [Depositing Fiat](/circle-mint/howtos/deposit-fiat). * Reviewed the conceptual model in [Institutional API](/circle-mint/concepts/institutional-api). ## Step 1: Create an external entity Call `POST /v1/externalEntities` with the entity's `businessName`, `businessUniqueIdentifier` (tax ID), `identifierIssuingCountryCode`, and `address`. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/externalEntities \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "businessName": "Acme Treasury Ltd.", "businessUniqueIdentifier": "12-3456789", "identifierIssuingCountryCode": "US", "address": { "line1": "100 Market St", "city": "San Francisco", "district": "CA", "postalCode": "94105", "country": "US" } }' ``` The response returns HTTP 201 with `complianceState: PENDING`: ```json theme={null} { "data": { "walletId": "212000", "businessName": "Acme Treasury Ltd.", "businessUniqueIdentifier": "12-3456789", "identifierIssuingCountryCode": "US", "complianceState": "PENDING" } } ``` The `walletId` is returned at creation but remains unusable while the entity is in `PENDING` or `REJECTED`. Wait for the compliance decision before referencing the entity's wallet in any other endpoint. ## Step 2: Wait for the compliance decision Subscribe to the `externalEntities` webhook topic to receive the asynchronous compliance decision. On `ACCEPTED`, the payload confirms the entity's `walletId` is usable. On `REJECTED`, the entity cannot be used: resubmit a new entity with corrected information or [contact Circle](https://www.circle.com/mint-contact). As a fallback, poll `GET /v1/externalEntities/{walletId}` with the `walletId` returned at creation until `complianceState` changes. ```json theme={null} { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802", "notificationType": "externalEntities", "version": 1, "externalEntity": { "walletId": "212000", "businessName": "Acme Treasury Ltd.", "businessUniqueIdentifier": "12-3456789", "identifierIssuingCountryCode": "US", "complianceState": "ACCEPTED" } } ``` ## Step 3: Mint on behalf of the entity Use the entity's `walletId` to scope wire instructions, then watch for the deposit to land on the entity wallet. ### 3.1. Generate entity-scoped wire instructions Call `GET /v1/businessAccount/banks/wires/{id}/instructions?walletId=`, passing the linked bank `id` in the path and the entity wallet in the query string. ```bash theme={null} curl "https://api-sandbox.circle.com/v1/businessAccount/banks/wires/9d1fa351-b24d-442a-8aa5-e717db1ed636/instructions?walletId=212000" \ -H "Authorization: Bearer $API_KEY" ``` The response returns the entity-scoped `trackingRef`; deposits that include it are credited to the entity wallet: ```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" } } } ``` ### 3.2. Have the end client wire fiat to those instructions Share the beneficiary details and the entity-scoped `trackingRef` with your end client. Optionally include a `customerExternalRef` matching `.*(EXT[A-Z0-9]{18}).*` in the bank memo for Distributor-side reconciliation. ### 3.3. Watch the `deposits` webhook for the credit Circle fires a `deposits` event with `destination.id` equal to the entity's `walletId` once the wire settles. The USDC or EURC is then available in the entity wallet. ```json theme={null} { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802", "notificationType": "deposits", "version": 1, "deposit": { "id": "b8627ae8-732b-4d25-b947-1df8f4007a29", "sourceWalletId": "212000", "destination": { "type": "wallet", "id": "212000" }, "amount": { "amount": "50000.00", "currency": "USD" }, "status": "complete", "trackingRef": "CIR22FEP33", "createDate": "2026-05-01T14:20:30.000Z", "updateDate": "2026-05-01T14:21:12.000Z" } } ``` ## Step 4: Transfer USDC onchain on behalf of the entity Move USDC into or out of the entity wallet onchain by scoping the deposit address and the transfer source to the entity `walletId`. ### 4.1. Generate an entity-scoped deposit address for inbound transfers Call `POST /v1/businessAccount/wallets/addresses/deposit` with the entity wallet in the request body. Mint supports the same blockchains for institutional subaccount wallets as for the Distributor's primary wallet. See [Supported Chains and Currencies](/circle-mint/references/supported-chains-and-currencies). ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/deposit \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "walletId": "212000", "currency": "USD", "chain": "ARC" }' ``` ### 4.2. Send an outbound transfer from the entity wallet First, allowlist the recipient with `POST /v1/businessAccount/wallets/addresses/recipient`. The address must be approved by an account administrator through the Mint Console before it can be used. Watch the [`addressBookRecipients`](/circle-mint/references/webhook-notifications#addressbookrecipients) webhook for the `active` status that signals approval. Then create the transfer with `POST /v1/businessAccount/transfers`, setting `source.type` to `wallet` and `source.id` to the entity's `walletId`. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/transfers \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "type": "wallet", "id": "212000" }, "destination": { "type": "verified_blockchain", "addressId": "addr_01HZ..." }, "amount": { "amount": "1000.00", "currency": "USD" }, "idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde" }' ``` ## Step 5: Redeem on behalf of the entity Call `POST /v1/businessAccount/payouts` with `source.type` set to `wallet`, `source.id` set to the entity's `walletId`, and `destination.type` set to `wire` with the linked fiat account `id`. Watch the `payouts` webhook for `complete` or `failed`. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/businessAccount/payouts \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "type": "wallet", "id": "212000" }, "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636" }, "amount": { "amount": "10000.00", "currency": "USD" }, "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7" }' ``` The Institutional Direct fee is deducted at the point of redemption, so the `toAmount` returned on the payout reflects the net amount the entity's bank receives: ```json theme={null} { "clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802", "notificationType": "payouts", "version": 1, "payout": { "id": "c0f88a17-2a8b-4d51-9c4e-c8d3f2cfa011", "sourceWalletId": "212000", "destination": { "type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636" }, "amount": { "amount": "10000.00", "currency": "USD" }, "toAmount": { "amount": "9990.00", "currency": "USD" }, "fees": { "amount": "10.00", "currency": "USD" }, "status": "complete", "createDate": "2026-05-01T14:20:30.000Z", "updateDate": "2026-05-01T14:25:11.000Z" } } ``` ## Step 6: Query entity-scoped activity Filter list endpoints by the entity's `walletId` (or `sourceWalletId` / `destinationWalletId` where supported) to retrieve activity scoped to a single entity: * Balance: `GET /v1/businessAccount/balances?walletId=` * Deposits: `GET /v1/businessAccount/deposits?walletId=` * Transfers: `GET /v1/businessAccount/transfers?walletId=`. Use `sourceWalletId` or `destinationWalletId` to filter by direction. * Payouts: `GET /v1/businessAccount/payouts?sourceWalletId=` * Deposit addresses: `GET /v1/businessAccount/wallets/addresses/deposit?walletId=` Omitting `walletId` on these endpoints returns activity for the Distributor's primary wallet (`masterWalletId`), not all entities. Always pass the entity `walletId` when you want entity-scoped results. ## Endpoint reference The following table maps each operation in this guide to its endpoint and the location of the `walletId` parameter. | Operation | Method and path | `walletId` location | | ------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------- | | Create external entity | `POST /v1/externalEntities` | Body | | List external entities | `GET /v1/externalEntities` | Optional filter via `businessUniqueIdentifier` | | Get external entity | `GET /v1/externalEntities/{walletId}` | Path parameter | | Get entity wire instructions | `GET /v1/businessAccount/banks/wires/{id}/instructions?walletId=…` | Query string | | Generate entity deposit address | `POST /v1/businessAccount/wallets/addresses/deposit` | Body (`walletId`) | | List entity deposit addresses | `GET /v1/businessAccount/wallets/addresses/deposit?walletId=…` | Query string | | Transfer onchain from entity | `POST /v1/businessAccount/transfers` | Body (`source.id` with `source.type: wallet`) | | List entity transfers | `GET /v1/businessAccount/transfers?walletId=…` | Query string | | Redeem from entity | `POST /v1/businessAccount/payouts` | Body (`source.id` with `source.type: wallet`) | | List entity payouts | `GET /v1/businessAccount/payouts?sourceWalletId=…` | Query string | | List entity deposits | `GET /v1/businessAccount/deposits?walletId=…` | Query string | | Entity balance | `GET /v1/businessAccount/balances?walletId=…` | Query string | ## See also * [Institutional API](/circle-mint/concepts/institutional-api): conceptual model for Distributors, external entities, and per-entity wallets. * [Webhook notifications](/circle-mint/references/webhook-notifications#externalentities): schema and delivery for the `externalEntities` callback. * [Error codes](/api-reference/circle-mint/error-codes): synchronous and asynchronous failure modes. * [Supported payment rails](/circle-mint/references/supported-payment-rails): fiat rails available for entity wire deposits and redemptions. * [Depositing Fiat](/circle-mint/howtos/deposit-fiat): linked bank account setup and wire deposit basics. # 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: Mint and redeem cirBTC Source: https://developers.circle.com/circle-mint/quickstarts/mint-and-redeem-cirbtc Deposit BTC to mint cirBTC, transfer cirBTC onchain, and redeem cirBTC back to BTC using the Circle Mint API. Complete a full [cirBTC](/assets/what-is-cirbtc) mint-and-redeem cycle in the Circle Mint sandbox: create a BTC deposit address, deposit BTC to mint cirBTC, transfer cirBTC onchain to an Ethereum address, and redeem cirBTC back to BTC. ## Prerequisites Before you begin, complete the [account and API key setup](/circle-mint/quickstarts/getting-started). Substitute your sandbox API key for `${YOUR_API_KEY}` in the examples below. ## Step 1: Create a BTC deposit address ### 1.1. Create a deposit address Create a deposit address on the Bitcoin blockchain using the [create a deposit address](/api-reference/circle-mint/account/create-business-deposit-address) endpoint. ```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": "unique-id-1", "currency": "CIRBTC", "chain": "BTC" }' ``` Expected response: ```json theme={null} { "data": { "id": "b4b843a0-0297-5a5b-bf5e-8e0375642f8e", "address": "tb1qexampleaddress0123456789abcdef", "currency": "CIRBTC", "chain": "BTC" } } ``` Save the `address` from the response. ### 1.2. Send BTC to the deposit address Send BTC to the deposit address to initiate minting. In the sandbox, use the [Circle faucet](https://faucet.circle.com/) to obtain testnet BTC and send it to your deposit address. You can also use the sandbox environment at [app-smokebox.circle.com](https://app-smokebox.circle.com) to test API interactions. ## Step 2: Verify your cirBTC balance After BTC reaches four-block confirmation, cirBTC is credited to your account. 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": "0.00100000", "currency": "CIRBTC" }], "unsettled": [] } } ``` The `available` balance confirms that your BTC deposit minted cirBTC successfully. cirBTC uses a fast-mint mechanism. After four-block BTC confirmation (\~40 minutes), cirBTC is transferred from a pre-minted pool to your account. The underlying onchain reserve transfer completes in the background. ## Step 3: Transfer cirBTC to an Ethereum address Send cirBTC from your Circle Mint account to an external Ethereum address. This step requires two API calls: create a recipient address, then create a transfer. ### 3.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. ```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": "CIRBTC", "description": "External Ethereum wallet for cirBTC" }' ``` Expected response: ```json theme={null} { "data": { "id": "cfa01bb0-d166-5506-a48a-56f2beab559f", "address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1", "chain": "ETH", "currency": "CIRBTC", "description": "External Ethereum wallet for cirBTC" } } ``` Save the `id` from the response. You need it as the `addressId` in the next step. ### 3.2. Create a transfer Send cirBTC 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": "CIRBTC", "amount": "0.00050000" } }' ``` Expected response: ```json theme={null} { "data": { "id": "a3e2c8d1-4f6b-5a9e-b1c3-7d8e9f0a2b4c", "source": { "type": "wallet", "id": "1016875042" }, "destination": { "type": "blockchain", "address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1", "chain": "ETH" }, "amount": { "amount": "0.00050000", "currency": "CIRBTC" }, "status": "pending" } } ``` The transfer starts in `pending` status and reaches `complete` after sufficient [blockchain confirmations](/circle-mint/references/blockchain-confirmations). You can poll the [get a transfer](/api-reference/circle-mint/account/get-business-transfer) endpoint with the transfer `id` to check its status. ## Step 4: Redeem cirBTC back to BTC To redeem cirBTC, create a BTC recipient address and then transfer cirBTC to it. Circle burns the cirBTC and releases native BTC to the specified address. ### 4.1. Create a BTC recipient address Register a Bitcoin withdrawal address using the [create a recipient address](/api-reference/circle-mint/account/create-business-recipient-address) endpoint: ```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-4", "address": "tb1qyourbtcwithdrawaladdress", "chain": "BTC", "currency": "CIRBTC", "description": "BTC withdrawal address" }' ``` Expected response: ```json theme={null} { "data": { "id": "f7a39c2e-81d4-5b0f-a6e8-3c9d1f4e5b7a", "address": "tb1qyourbtcwithdrawaladdress", "chain": "BTC", "currency": "CIRBTC", "description": "BTC withdrawal address" } } ``` ### 4.2. Create a redemption transfer Transfer your remaining cirBTC balance to the BTC 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": "unique-id-5", "destination": { "type": "verified_blockchain", "addressId": "f7a39c2e-81d4-5b0f-a6e8-3c9d1f4e5b7a" }, "amount": { "currency": "CIRBTC", "amount": "0.00050000" } }' ``` Expected response: ```json theme={null} { "data": { "id": "c9b1d4e7-3a2f-4c8d-b5e6-1f0a2b3c4d5e", "amount": { "amount": "0.00050000", "currency": "CIRBTC" }, "status": "pending" } } ``` Circle burns the cirBTC and releases the equivalent BTC to your withdrawal address after sufficient blockchain confirmations. See [Blockchain confirmations](/circle-mint/references/blockchain-confirmations) for expected confirmation times. ## Step 5: Verify the round trip Check your final balance to confirm the transfers 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.00000000", "currency": "CIRBTC" }], "unsettled": [] } } ``` You completed the full cirBTC cycle: 1. Deposited BTC to mint 0.001 cirBTC. 2. Transferred 0.0005 cirBTC onchain to an Ethereum address. 3. Redeemed 0.0005 cirBTC back to BTC. Your available cirBTC balance returns to zero, confirming every token is accounted for. The Circle faucet used in Step 1 is only available for testnet. In production, send real BTC to the deposit address you created in Step 1. All other API calls in this guide work the same in production. See [Sandbox and Testing](/circle-mint/references/sandbox-and-testing) for details on transitioning. # Quickstart: Reserve and repay a settlement advance Source: https://developers.circle.com/circle-mint/quickstarts/settlement-advance Check your credit line, reserve funds, send a wire and upload proof, and observe disbursement and repayment for a Settlement Advance draw. This quickstart walks through a complete Settlement Advance draw lifecycle: you check the credit line, reserve funds, send a wire and upload proof, observe disbursement and accrued fees, and then repay by wire and confirm the repayment. For the conceptual model behind these calls, see the [Credit API](/circle-mint/concepts/credit-api) concept page. ## Prerequisites Before you begin, make sure that you've: * Contacted your Circle representative to activate Settlement Advance on your Circle Mint account. * Confirmed your Circle Mint wallet holds the required USDC `minBalance` for your credit line. * Configured API authentication per [Getting Started](/circle-mint/quickstarts/getting-started). Examples below use `$API_KEY` and the base URL `https://api-sandbox.circle.com`. * Identified the `fiatAccountId` of the wire bank account from which you'll send the inbound wire to Circle. ## Step 1. Check the credit line Call `GET /v1/credit` to confirm your credit line is active and has sufficient capacity for the draw. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit \ -H "Authorization: Bearer $API_KEY" ``` ```json 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" } } ``` Before proceeding, confirm: * `product` is `settlementAdvance`. * `available.amount` covers the draw amount you plan to reserve. * `validationErrors` is an empty array. For the meaning of each field, see the [credit-line model](/circle-mint/concepts/credit-api#the-credit-line-model). ## Step 2. Get wire repayment instructions Call `GET /v1/credit/repaymentAccounts/{fiatAccountId}` to retrieve wire instructions for the repayment account tied to your wire bank account. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/repaymentAccounts/b8627ae8-732b-4d25-b947-1df8f4007a29 \ -H "Authorization: Bearer $API_KEY" ``` ```json 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": "CRYPTO99", "routingNumber": "999999999", "accountNumber": "3302726104", "currency": "USD", "name": "CIRCLE BANKING PARTNER", "address": "100 MAIN STREET", "city": "NEW YORK", "postalCode": "10001", "country": "US" } } } } ``` The repayment account is `unverified` until Circle matches the first incoming wire repayment to it, after which it transitions to `active`. Cache the `wireInstructions` block—you reuse it when repaying in Step 6. In the sandbox, simulate a matching wire and verify the account by posting a mock repayment. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/mocks/repayments \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amount": { "amount": "100000.00", "currency": "USD" } }' ``` ```json theme={null} { "data": { "trackingRef": "CIR3XBZZ4N", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "pending" } } ``` ## Step 3. Reserve funds for the advance Call `POST /v1/credit/transfers/reserveFunds` to hold capacity against your credit line while you initiate the supporting wire. To use Credit Express, include the optional `destination` field to disburse directly to a verified address from your Circle Mint recipient address book; omit it to land the disbursement in your Mint wallet. ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/transfers/reserveFunds \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "amount": { "amount": "100000.00", "currency": "USD" }, "destination": { "type": "verified_blockchain", "addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab" } }' ``` ```json theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "funds_reserved", "expiresAt": "2024-03-20T15:00:00.000Z", "outstanding": { "amount": "100000.00", "currency": "USD" }, "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—it identifies this draw for the remaining steps. Reservations expire 30 minutes after creation and only one can be open per credit line at a time; for the full set of constraints, see [Settlement Advance lifecycle](/circle-mint/concepts/credit-api#settlement-advance-lifecycle). If you can't send the wire before the reservation expires, cancel it with [`PUT /v1/credit/transfers/{id}/cancelReserve`](/api-reference/circle-mint/credit/cancel-credit-transfer-reserve) to free the capacity immediately. Settlement Advance cannot use `POST /v1/credit/transfers` to create a draw. That endpoint is for Line of Credit only and returns HTTP 400 for Settlement Advance credit lines. Use `reserveFunds` followed by `requestReservedFunds`. ## Step 4. Send the wire and upload wire proof Using the wire instructions from Step 2, instruct your bank to send a wire to Circle for the reserved amount. Then upload proof of the wire to request disbursement by calling `PUT /v1/credit/transfers/{id}/requestReservedFunds` with a `multipart/form-data` request. Accepted file types are `application/pdf`, `image/jpeg`, and `image/png`. ```bash theme={null} curl -X PUT https://api-sandbox.circle.com/v1/credit/transfers/b3d9d2d5-4c12-4946-a09d-953e82fae2b0/requestReservedFunds \ -H "Authorization: Bearer $API_KEY" \ -F "fileName=wire-proof.pdf" \ -F "file=@wire-proof.pdf" ``` ```json theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "requested", "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-20T14:45:00.000Z" } } ``` The transfer moves from `funds_reserved` to `requested`. Circle's Treasury team manually reviews the wire proof. In the sandbox the request auto-approves quickly; in production, approval typically takes 20 minutes to 2 hours. ## Step 5. Observe disbursement and fee accrual Once Treasury approves the request, the transfer moves to `disbursed` and `recurringFee` begins accruing daily against the outstanding balance. ### 5.1. Poll the transfer Call `GET /v1/credit/transfers/{id}` to inspect the disbursement, due date, and accrued fees. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/transfers/b3d9d2d5-4c12-4946-a09d-953e82fae2b0 \ -H "Authorization: Bearer $API_KEY" ``` ```json theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "disbursed", "outstanding": { "amount": "100040.00", "currency": "USD" }, "fees": { "total": { "amount": "40.00", "currency": "USD" }, "unpaid": { "amount": "40.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 is the principal plus accrued fees. Each day the credit line is open with an outstanding balance, `recurringFee` is applied to the principal and added to `fees.total`. The `dueDate` is 7 days after `disbursedDate` for Settlement Advance. If you reserved with a Credit Express `destination`, the `blockchainDestination` block tracks the onchain leg separately from the credit transfer. The `blockchainDestination.transferId` references the underlying Circle Mint transfer once disbursement initiates onchain. ### 5.2. Subscribe to webhooks For asynchronous updates, subscribe to `creditTransfers`, `creditFees`, and `creditRepayments` in the Circle Mint Console. Webhook payloads mirror the corresponding `GET` endpoints. See [webhook topics](/circle-mint/concepts/credit-api#webhook-topics) for what each topic publishes. ## Step 6. Repay by wire Send a wire from the bank account from Step 2 to Circle for the outstanding amount (principal plus accrued fees). Use the `wireInstructions` you cached from Step 2. In the sandbox, simulate the wire by posting a mock repayment: ```bash theme={null} curl -X POST https://api-sandbox.circle.com/v1/credit/mocks/repayments \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amount": { "amount": "100040.00", "currency": "USD" } }' ``` ```json theme={null} { "data": { "trackingRef": "CIR3XBZZ4N", "amount": { "amount": "100040.00", "currency": "USD" }, "status": "pending" } } ``` ## Step 7. Confirm the repayment List repayments filtered to your transfer by calling `GET /v1/credit/repayments?transferId={id}` and confirm a record exists with `type: fiat` and `status: completed`. ```bash theme={null} curl "https://api-sandbox.circle.com/v1/credit/repayments?transferId=b3d9d2d5-4c12-4946-a09d-953e82fae2b0" \ -H "Authorization: Bearer $API_KEY" ``` ```json theme={null} { "data": [ { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "transferId": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "repaymentAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29", "amountApplied": { "amount": "100040.00", "currency": "USD" }, "paymentAmount": { "amount": "100040.00", "currency": "USD" }, "type": "fiat", "status": "completed", "settlementDate": "2024-03-22T09:30:00.000Z", "createDate": "2024-03-22T09:30:00.000Z", "updateDate": "2024-03-22T09:30:00.000Z" } ] } ``` `paymentAmount` is the total wire received and `amountApplied` is the portion applied to this transfer's outstanding balance (principal plus fees). `repaymentAccountId` is the fiat account the wire was matched to—the same account whose wire instructions you retrieved in Step 2. Then call `GET /v1/credit/transfers/{id}` again and confirm the transfer is `paid`. ```bash theme={null} curl https://api-sandbox.circle.com/v1/credit/transfers/b3d9d2d5-4c12-4946-a09d-953e82fae2b0 \ -H "Authorization: Bearer $API_KEY" ``` ```json theme={null} { "data": { "id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0", "amount": { "amount": "100000.00", "currency": "USD" }, "status": "paid", "outstanding": { "amount": "0.00", "currency": "USD" }, "fees": { "total": { "amount": "40.00", "currency": "USD" }, "unpaid": { "amount": "0.00", "currency": "USD" } }, "dueDate": "2024-03-28T10:00:00.000Z", "paidDate": "2024-03-22T09:30:00.000Z", "disbursedDate": "2024-03-21T10:00:00.000Z", "createDate": "2024-03-20T14:30:00.000Z", "updateDate": "2024-03-22T09:30:00.000Z" } } ``` The transfer now shows `status: paid`, `outstanding` is zero, and `fees.unpaid` is zero—the draw lifecycle is complete. Settlement Advance does not support crypto repayment. `POST /v1/credit/cryptoRepayment` returns HTTP 400 for Settlement Advance credit lines. Repay by wire only. * [Getting Started](/circle-mint/quickstarts/getting-started): API authentication and account setup. # 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 | | Arc testnet | **1** | under 1 second | | Avalanche C-Chain | **1** | \~2 seconds | | Base | **12 ETH Blocks** | \~3 to 9 minutes | | Bitcoin | **4** | \~40 minutes | | Celo | **12 ETH Blocks** | \~3 to 9 minutes | | Codex | **12 ETH Blocks** | \~3 to 9 minutes | | Cronos | **1** | under 1 second | | 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 | | X Layer | **65 ETH Blocks** | \~15 to 20 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. # CAMT.053 daily statements Source: https://developers.circle.com/circle-mint/references/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/references/camt053-statements#retrieve-the-report) through the Circle Mint API and parse it to reconcile balances, match transactions, and integrate with your treasury systems. This page covers Circle Mint accounts. For Managed Payments, request the `camt_managed` report type instead of `camt053`. See [Managed Payments reporting](/cpn/managed-payments/references/reporting). ## Availability and SLA * **Coverage**: Each report covers the prior calendar day from 00:00 through 24:00 UTC. * **When it's ready**: Circle delivers the report by 06:00 UTC the next day, typically around 02:00 UTC. * **One file, multiple currencies**: Circle delivers USDC and EURC statements 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 ### 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, check that the opening balance plus the sum of credits and debits aligns with the 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 ``. Circle preserves the full token identifier (`USDC` or `EURC`) on each transaction line. At the transaction level it follows the path `Ntry` → `NtryDtls` → `TxDtls` → `SplmtryData` → [`CircleTxn`](/circle-mint/references/camt053-statements#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 the following: * `Amt`: Amount and ISO currency on the amount (`USD` or `EUR`). * `CdtDbtInd`: `CRDT` (credit) or `DBIT` (debit). * `Sts`: Booking status appears in `Cd` with the following possible values: * `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`. Unmapped types appear as `Unknown`. ### Transaction type codes The value at `BkTxCd` → `Prtry` → `Cd` is case-sensitive. Circle Mint and Managed Payments use different code sets, so select the table that matches the `reportType` you requested. Your parser should continue accepting unknown values so that a newly introduced transaction type doesn't stop reconciliation. The `camt053` report emits the following values: | Code | Transaction category | | ---------------------------- | ------------------------------------------ | | `Escheatment` | Unclaimed-property debit | | `Mint` | Fiat or wrapped-token mint | | `Redeem` | Fiat or wrapped-token redemption | | `Crypto Payout` | Stablecoin payout | | `Crypto Refund` | Stablecoin payment refund | | `Crypto Payment` | Stablecoin payment | | `Crypto Payout Reversal` | Reversed or canceled stablecoin payout | | `Crypto Payment Fee` | Stablecoin payment fee | | `Crypto Payout Fee` | Stablecoin payout fee | | `Borrow` | DeFi credit borrow | | `Repayment` | DeFi credit repayment | | `Add Collateral` | DeFi collateral deposit | | `Advance` | Line-of-credit advance | | `Advance Repayment` | Line-of-credit repayment | | `Advance Overpayment` | Line-of-credit overpayment refund | | `Advance Fee` | Line-of-credit fee | | `Unsupported Funds Recovery` | Recovery of unsupported funds | | `Send On-chain` | Onchain send | | `Receive On-chain` | Onchain receive | | `Unwithdrawal` | Reversed fiat withdrawal | | `Redemption Fee` | Fiat redemption fee | | `Trade Settlement` | Foreign-exchange trade settlement | | `Trade Withdrawal` | Foreign-exchange trade withdrawal | | `Trade Mint` | Foreign-exchange trade mint | | `Trade Unwithdrawal` | Reversed foreign-exchange trade withdrawal | | `Convert Withdrawal` | Currency-conversion withdrawal | | `Convert Mint` | Currency-conversion mint | | `Invoice Transfer` | Invoice transfer | | `Subwallet Funding` | Legacy code name for sub-account funding | | `CeFi Funding` | Centralized-finance funding | | `CeFi Withdrawal` | Centralized-finance withdrawal | | `Wallet Transfer` | Other wallet transfer | | `Unknown` | Transaction without a mapped category | The `camt_managed` report emits compact proprietary codes. This column shows the corresponding transaction category for reconciliation: | Code | Transaction category | | -------------------------- | ---------------------------------------- | | `Escheatment` | Unclaimed-property debit | | `Fiat` | Fiat mint or redemption | | `CryptoPayout` | Stablecoin payout | | `CryptoRefund` | Stablecoin payment refund | | `CryptoPayment` | Stablecoin payment deposit | | `CryptoPayoutReversal` | Reversed or canceled stablecoin payout | | `CryptoPaymentFee` | Stablecoin payment fee | | `CryptoPayoutFee` | Stablecoin payout fee | | `Borrow` | DeFi credit borrow | | `Repayment` | DeFi credit repayment | | `AddCollateral` | DeFi collateral deposit | | `SettlementCredit` | Line-of-credit advance | | `AdvanceRepayment` | Line-of-credit repayment | | `AdvanceOverpayment` | Line-of-credit overpayment refund | | `LineOfCreditAdvanceFee` | Line-of-credit fee | | `UnsupportedFundsRecovery` | Recovery of unsupported funds | | `SendOnChain` | Onchain send | | `ReceiveOnChain` | Onchain receive | | `Unwithdrawal` | Reversed fiat withdrawal | | `RedemptionFee` | Fiat redemption fee | | `TradeSettlement` | Foreign-exchange trade settlement | | `TradeWithdrawal` | Foreign-exchange trade withdrawal | | `TradeMint` | Foreign-exchange trade mint | | `TradeUnwithdrawal` | Reversed foreign-exchange withdrawal | | `ConvertWithdrawal` | Currency-conversion withdrawal | | `ConvertMint` | Currency-conversion mint | | `InvoiceTransfer` | Invoice transfer | | `SubwalletFunding` | Legacy code name for sub-account funding | | `CeFiFunding` | Centralized-finance funding | | `CeFiWithdrawal` | Centralized-finance withdrawal | | `WalletTransfer` | Other wallet transfer | | `Unknown` | Transaction without a mapped category | ### 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 isn't exhaustive. Your parser must tolerate additional elements, and any of the following fields may be absent on a given line. * `TransactionId`: Identifier for the movement. * `JobId`: Related job identifier. * `Token`: Full token identifier (`USDC` or `EURC`). * `CustomReferenceId`: Customer-provided reference when supplied. * `ExternalReferenceId`: Electronic Funds Transfer (EFT) 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 when populated, for example, fiat account or blockchain address. * `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 your records. ## Example truncated statement The following example shows the header, one statement with opening and closing balances, and one abbreviated entry. It's 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
``` ## See also * [Webhook notifications](/circle-mint/references/webhook-notifications): Subscribe to event notifications for transactions and account activity. * [Supported payment rails](/circle-mint/references/supported-payment-rails): Review the bank rails and blockchains available for funding and withdrawals. * [Error codes](/api-reference/circle-mint/error-codes): Look up API error codes returned by Circle Mint endpoints. # 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. ## Debug requests with API Logs Circle stores every API request and response your account makes and surfaces them in the [API Logs page](https://app-sandbox.circle.com/developer/logs) of the Mint Console (Developer Tab → Logs). Logs are retained for seven days and include the HTTP status, path, request ID, idempotency key, user agent, origin, timestamp, and full request and response bodies. You can filter by request ID, resource ID, idempotency key, date range, status, HTTP method, and path. Sensitive values—payment methods and personally identifiable information—appear as `[redacted]` in stored payloads. Contact your Circle account manager if you need access to redacted data for a specific debugging session. # 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). Arc (`ARC`) is only available in the sandbox environment (`api-sandbox.circle.com`), where the chain code refers to Arc testnet. ## USDC | Chain | API Chain Code | API Currency Code | | ------------------ | -------------- | ----------------- | | Algorand | `ALGO` | `USD` | | Aptos | `APTOS` | `USD` | | Arbitrum | `ARB` | `USD` | | Arc testnet | `ARC` | `USD` | | Avalanche C-Chain | `AVAX` | `USD` | | Base | `BASE` | `USD` | | Celo | `CELO` | `USD` | | Codex | `CODEX` | `USD` | | Cronos | `CRONOS` | `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` | | X Layer | `XLAYER` | `USD` | | XDC | `XDC` | `USD` | | XRPL | `XRP` | `USD` | | ZKsync Era | `ZKS` | `USD` | ## EURC | Chain | API Chain Code | API Currency Code | | ----------------- | -------------- | ----------------- | | Arc testnet | `ARC` | `EUR` | | Avalanche C-Chain | `AVAX` | `EUR` | | Base | `BASE` | `EUR` | | Cronos | `CRONOS` | `EUR` | | Ethereum | `ETH` | `EUR` | | Solana | `SOL` | `EUR` | | Stellar | `XLM` | `EUR` | | World Chain | `WORLDCHAIN` | `EUR` | ## cirBTC | Chain | API Chain Code | API Currency Code | | -------- | -------------- | ----------------- | | Ethereum | `ETH` | `CIRBTC` | For an overview of cirBTC and how it works, see [What is cirBTC?](/assets/what-is-cirbtc). ## 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. # Supported payment rails Source: https://developers.circle.com/circle-mint/references/supported-payment-rails Reference for the fiat payment rails Circle Mint supports—Fedwire, SWIFT, RTP, SEPA, SPEI, CHATS, PIX, CUBIX, and book transfers—including currencies, regions, settlement timing, and API endpoints. Circle Mint supports a range of fiat payment rails for funding deposits and sending payouts. Use this reference to decide how a counterparty should fund a deposit or receive a payout based on currency, region, and the settlement speed you need. Circle auto-routes deposits and payouts submitted to `/v1/businessAccount/banks/wires` to the appropriate rail based on the linked bank's capabilities, while PIX and CUBIX have dedicated endpoints. Every rail below supports both deposits and payouts. ## Rail comparison | Rail | Currencies | Region | Settlement time | API endpoint | | ------------- | ---------- | ------------------------ | ----------------------------------------------------------- | --------------------------------- | | Fedwire | USD | United States (domestic) | 1–3 business days; same business day if before daily cutoff | `/v1/businessAccount/banks/wires` | | SWIFT | USD, EUR | Cross-border | 1–3 business days; longer with intermediary banks | `/v1/businessAccount/banks/wires` | | RTP | USD | United States (domestic) | Near-instant, 24/7 | `/v1/businessAccount/banks/wires` | | SEPA | EUR | SEPA zone (domestic) | Same or next business day | `/v1/businessAccount/banks/wires` | | SEPA Instant | EUR | SEPA zone (domestic) | Near-instant, 24/7 | `/v1/businessAccount/banks/wires` | | SPEI | MXN | Mexico (domestic) | Near-instant | `/v1/businessAccount/banks/wires` | | CHATS | HKD | Hong Kong (domestic) | Under 5 minutes | `/v1/businessAccount/banks/wires` | | Book transfer | USD, EUR | Domestic same-bank | Near-instant, banking hours | `/v1/businessAccount/banks/wires` | | PIX | BRL | Brazil (domestic) | Near-instant, 24/7 | `/v1/businessAccount/banks/pix` | | CUBIX | USD | Contact Circle | Contact Circle | `/v1/businessAccount/banks/cubix` | ## USD rails ### Fedwire Fedwire is the domestic US wire rail that runs over the Federal Reserve's wholesale wire system. Wires settle same business day when submitted before the daily Fedwire cutoff; otherwise they settle the next business day. The linked bank must have a US ABA routing number. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts. ### SWIFT SWIFT is the international wire rail and supports both USD and EUR. The linked bank must have a SWIFT/BIC, and for accounts in SEPA-zone countries an IBAN is also required. Settlement typically takes 1–3 business days but can take longer when intermediary banks are involved. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts. ### RTP RTP is the Real-Time Payments network in the United States. It is domestic-only and settles near-instantly, 24/7. There is no separate endpoint: Circle auto-routes deposits and payouts submitted to `/v1/businessAccount/banks/wires` over RTP when the linked US bank participates in the network. Per-rail transaction limits are subject to Circle's banking partner configuration; contact Circle to confirm the limit. ### CUBIX CUBIX is a USD rail exposed through a dedicated endpoint, `/v1/businessAccount/banks/cubix`. Use this endpoint to link a CUBIX-capable account and to submit deposits and payouts. Contact Circle to confirm the geographic scope, settlement timing, and account prerequisites for CUBIX before integrating. ### Book transfer When the linked bank account and a Circle account are held at the same banking partner, Mint can route deposits and payouts as a book-to-book transfer in that bank. These transfers are typically near-instant during banking hours and support USD and EUR. There is no separate endpoint: Mint chooses this path automatically when the linked bank matches. ## EUR rails ### SEPA SEPA is the Single Euro Payments Area credit transfer rail for accounts in SEPA-zone countries. Settlement is same or next business day. The linked bank must be in a SEPA country and provide an IBAN. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts. ### SEPA Instant SEPA Instant settles near-instantly, 24/7, subject to the linked bank's participation in the SEPA Instant scheme. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts; Circle auto-routes through SEPA Instant when the linked bank supports it. Per-rail transaction limits are subject to scheme and banking partner configuration; contact Circle to confirm the limit. ## MXN rails ### SPEI SPEI (Sistema de Pagos Electrónicos Interbancarios) is Mexico's domestic real-time payment system. Settlement is near-instant. The linked bank must be configured for SPEI. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts. ## HKD rails ### CHATS CHATS (Clearing House Automated Transfer System) is Hong Kong's real-time gross settlement system. Settlement is under 5 minutes. The linked bank must participate in CHATS. Use `/v1/businessAccount/banks/wires` to link the account and to submit payouts. Per-rail transaction limits are subject to scheme and banking partner configuration; contact Circle to confirm the limit. ## BRL rails ### PIX PIX is Brazil's instant payment system, operated by the Banco Central do Brasil. Settlement is near-instant, 24/7. The linked Brazil bank must be enrolled for PIX. PIX uses a dedicated endpoint surface: `/v1/businessAccount/banks/pix` to link an account and to submit payouts, and `/v1/businessAccount/banks/pix/{id}/instructions` to retrieve deposit instructions for a linked PIX account. Once fiat is received and credited to your Circle Mint account, Circle typically completes onchain minting in 15 minutes. For the full lifecycle from fiat receipt to onchain credit, see [How minting works](/circle-mint/concepts/how-minting-works). # Travel rule compliance Source: https://developers.circle.com/circle-mint/references/travel-rule-compliance Reference for Travel Rule thresholds, schemas, payment reason codes, Virtual Asset Service Provider lookup, and failure modes that apply to Circle Mint Stablecoin Payouts. Travel Rule is a financial-crime regulation that requires financial institutions to exchange originator and beneficiary information on cross-counterparty fund transfers that exceed defined thresholds. The Financial Crimes Enforcement Network (FinCEN) sets the rule in the United States, the Monetary Authority of Singapore (MAS) sets the equivalent rule in Singapore under Notice PSN02, and the European Union sets it under the Transfer of Funds Regulation (Regulation (EU) 2023/1113), which applies to payouts booked through Circle SAS (`CIRCLE_FR`). This reference describes how Circle applies these rules to Stablecoin Payouts and the third-party transfers booked through the Circle Mint Core API. Travel Rule does not currently impose additional data requirements on Stablecoin Payins. ## Regional rules The Circle entity that books the payout determines which rule set applies. The location of the customer or recipient is not the trigger. | Circle entity | Trigger | Threshold | Notes | | ------------------------------ | ---------------------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Circle LLC (United States) | All third-party payouts on Travel Rule blockchains at or exceeding threshold | \$3,000 USD-equivalent | FinCEN. Originator identities required. | | Circle Singapore (`CIRCLE_SG`) | Every third-party payout | None. Applies to all amounts. | MAS PSN02. Originator identities, beneficiary identity, ownership, Virtual Asset Service Provider (VASP), and payment reason code all required. See [Ownership](#ownership-circle_sg-and-circle_fr) for the current self-hosted-wallet limitation. | | Circle SAS (`CIRCLE_FR`) | Every third-party payout | None. Applies to all amounts. | EU Transfer of Funds Regulation (Regulation (EU) 2023/1113). Originator identities, beneficiary identity, ownership, and Virtual Asset Service Provider (VASP) required. Beneficiaries can include an optional legal entity identifier (LEI). See [Ownership](#ownership-circle_sg-and-circle_fr) for the current self-hosted-wallet limitation. | The booking entity on your account, not the geography of either side of the transfer, decides which threshold and which fields apply. ## Schemas The fields below describe the data Circle collects to satisfy Travel Rule. Use the schema appropriate to your booking entity and the recipient type. ### Originator identities Applies to every Stablecoin Payout subject to Travel Rule: Circle LLC at the \$3,000 threshold and `CIRCLE_SG` and `CIRCLE_FR` for all amounts. The originator identity travels in the `source.identities[]` array on `POST /v1/payouts`. It identifies the sender of the funds, which is your business and, where applicable, the customer that originated the transfer. | Field | Type | Required | Description | | ------------------------ | ------ | -------------------------------- | ------------------------------------- | | `type` | string | Yes | `individual` or `business`. | | `name` | string | Yes | Full legal name. | | `addresses[]` | array | Yes | One or more address objects. | | `addresses[].line1` | string | Yes | Street address. | | `addresses[].line2` | string | No | Additional address detail. | | `addresses[].city` | string | Yes | City. | | `addresses[].district` | string | Yes for United States and Canada | State or province as a 2-letter code. | | `addresses[].postalCode` | string | Yes | Postal code. | | `addresses[].country` | string | Yes | ISO 3166-1 alpha-2 country code. | The following example shows a single business originator identity: ```json theme={null} { "source": { "type": "wallet", "id": "12345", "identities": [ { "type": "business", "name": "Acme Payments, Inc.", "addresses": [ { "line1": "1 Market Street", "line2": "Suite 400", "city": "San Francisco", "district": "CA", "postalCode": "94105", "country": "US" } ] } ] } } ``` ### Beneficiary identity (CIRCLE\_SG and CIRCLE\_FR) Applies to Address Book recipients used by `CIRCLE_SG`-booked and `CIRCLE_FR`-booked payouts. The beneficiary identity travels in the `identity` object on `POST /v1/addressBook/recipients`. It identifies the recipient. The schema captures legal name and, for `CIRCLE_FR` recipients, an optional legal entity identifier. Addresses are not part of this object. | Field | Type | Required | Description | | -------------- | ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Yes | `individual` or `business`. | | `firstName` | string | Yes when `type: individual` | Beneficiary first name. | | `lastName` | string | Yes when `type: individual` | Beneficiary last name. | | `businessName` | string | Yes when `type: business` | Beneficiary legal business name. | | `lei` | string | No. `CIRCLE_FR` only. | Legal entity identifier (LEI): a 20-character alphanumeric code (ISO 17442) that identifies a legal entity. Optional regardless of beneficiary type. Circle validates the format. Omitted for other entities. | Individual beneficiary: ```json theme={null} { "identity": { "type": "individual", "firstName": "Satoshi", "lastName": "Nakamoto" } } ``` Business beneficiary: ```json theme={null} { "identity": { "type": "business", "businessName": "Globex Holdings Pte. Ltd." } } ``` Business beneficiary with a legal entity identifier (`CIRCLE_FR`): ```json theme={null} { "identity": { "type": "business", "businessName": "Example Corp", "lei": "529900T8BM49AURSDO55" } } ``` Circle captures the beneficiary identity at the recipient level so every payout reuses the same verified data. After creation, you cannot modify `identity` with `PATCH`. Attempts return error code `2036`. ### Ownership (CIRCLE\_SG and CIRCLE\_FR) Applies to Address Book recipients used by `CIRCLE_SG` and `CIRCLE_FR`. The ownership data travels in the `ownership` object on `POST /v1/addressBook/recipients` and declares whether the recipient is your own wallet or a third party's wallet, and whether that wallet is hosted by a VASP or self-hosted. | Field | Type | Required | Description | | ---------------- | ------ | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | | `type` | string | Yes | `first_party` or `third_party`. | | `custody.type` | string | Yes | `hosted` or `self_hosted`. | | `custody.vaspId` | string | Yes when `custody.type: hosted`. Omit otherwise. | The VASP that holds the recipient wallet. Obtain values from `GET /v1/addressBook/vasps`. | Third-party hosted-wallet recipient: ```json theme={null} { "ownership": { "type": "third_party", "custody": { "type": "hosted", "vaspId": "f1c5e96a-2c0e-4f9c-bf63-9a8a2d3c1c12" } } } ``` The API schema accepts `custody.type: self_hosted` for `CIRCLE_SG` and `CIRCLE_FR`, but the risk layer denies these recipients today. Build against hosted wallets until self-hosted support ships. After creation, you cannot modify `ownership` with `PATCH`. Attempts return error code `2037`. ## Virtual asset service provider lookup `GET /v1/addressBook/vasps` returns the active set of virtual asset service providers (`VASPs`) available for your jurisdiction. The endpoint is available to `CIRCLE_SG` and `CIRCLE_FR` customers. Use the returned `id` as `ownership.custody.vaspId` when you register a hosted-wallet recipient. ```bash theme={null} curl -X GET https://api-sandbox.circle.com/v1/addressBook/vasps \ -H "Authorization: Bearer $API_KEY" ``` Sample response: ```json theme={null} { "data": [ { "id": "8f9a0c2e-1d3b-4a5f-9c7b-2e3d4f5a6b7c", "name": "Anchorage Digital" }, { "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "name": "Coinhako" }, { "id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "name": "Coinbase" }, { "id": "9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d", "name": "Paxos" }, { "id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f", "name": "Circle" }, { "id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90", "name": "Circle Singapore" }, { "id": "00000000-0000-0000-0000-000000000000", "name": "Off Network VASP" } ] } ``` The list is dynamic. Query it at runtime rather than hardcoding IDs. ## Payment reason codes `purposeOfTransfer` on `POST /v1/payouts` carries a payment reason code that describes why the funds are moving. The field is required for `CIRCLE_SG`-booked and `CIRCLE_FR`-booked payouts and is not part of Travel Rule data collection for Circle LLC. Values align with the Cross-Border Payments Network (CPN) payment reason codes, with one addition (`PMT000`) that is unique to Stablecoin Payouts and is intended for cases that do not match another code. `PMT006` is not valid for Stablecoin 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 | ## Supported blockchains Travel Rule currently applies to Stablecoin Payouts on the following blockchains: * Algorand (`ALGO`) * Aptos (`APTOS`) * Arbitrum (`ARB`) * Arc testnet (`ARC`, sandbox only) * Avalanche (`AVAX`) * Base (`BASE`) * Celo (`CELO`) * Ethereum (`ETH`) * NEAR (`NEAR`) * Optimism (`OP`) * Polygon PoS (`POLY`) * XRP Ledger (`XRP`) * Solana (`SOL`) * Stellar (`XLM`) Circle manages Travel Rule applicability per blockchain, and this set can evolve. For the per-product blockchain support matrix, see [Supported Chains and Currencies](/circle-mint/references/supported-chains-and-currencies). ## Failure modes A Travel Rule problem surfaces in one of two places: at submission time as a synchronous validation error, or after submission as an asynchronous risk decision. ### Synchronous validation errors Returned at `POST` time with an HTTP 4xx response. Fix the request and retry with a fresh `idempotencyKey`. Address Book validation spans the `2024`-`2037` range; `2036` and `2037` are called out separately because they cover `PATCH` attempts on fields that are immutable after creation. | Error code | Trigger | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `5020` | Missing or invalid `purposeOfTransfer` on a `CIRCLE_SG`-booked payout. | | `2024`-`2035` | Address Book recipient validation: missing `identity`, missing `ownership`, missing or invalid `custody.vaspId`, and related shape errors. | | `2036` | Attempt to `PATCH` `identity` on an existing Address Book recipient. | | `2037` | Attempt to `PATCH` `ownership` on an existing Address Book recipient. | ### Asynchronous risk evaluation The payout accepts at submission with `HTTP 201`, then the resource transitions to `failed`. The payload carries the risk decision: ```json theme={null} { "data": { "id": "b36cbf12-6ed1-47ed-9eb9-5874f8991ca8", "status": "failed", "errorCode": "transaction_denied", "riskEvaluation": { "decision": "denied", "reason": "3220" } } } ``` `reason: 3220` indicates a Travel Rule violation. Review your originator identities, beneficiary identity (`CIRCLE_SG` and `CIRCLE_FR`), `vaspId`, and `purposeOfTransfer` against this reference, then re-submit with a new `idempotencyKey`. ## Receiving Travel Rule data Regulated financial institutions can request originator identities on received transfers by adding `returnIdentities=true` to `GET /v1/payouts/{id}` and `GET /v1/businessAccount/transfers/{id}`: ```bash theme={null} curl -X GET "https://api-sandbox.circle.com/v1/payouts/{id}?returnIdentities=true" \ -H "Authorization: Bearer $API_KEY" ``` The response carries a maximum of 5 originator identity items per call. Originator data associates with the transfer once it reaches `complete`. Non-bank financial institutions do not receive originator identity data on inbound transfers. # Webhook notifications Source: https://developers.circle.com/circle-mint/references/webhook-notifications Reference for Circle Mint webhook event topics with example payloads. This page lists every Circle Mint webhook event topic and shows an example payload for each. Mint webhooks are delivered as Amazon SNS messages on the [v1 notification system](/api-reference/webhooks#notification-api-versions). To register a webhook endpoint, see [Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications). If you do not have a Circle Mint account, start with the [Getting Started](/circle-mint/quickstarts/getting-started) quickstart. ## Event types The table below lists every Circle Mint webhook topic and the resource each notification carries. The subsections that follow describe each topic, document its status values when applicable, and show a single example payload. | Topic | Resource | What it reports | | ---------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------- | | `wire` | Wire bank account | Lifecycle of a linked wire bank account: `pending`, `complete`, or `failed`. | | `deposits` | Deposit | Settlement of a fiat deposit (mint) to your Circle Mint balance. | | `payouts` | Payout | Lifecycle of a fiat redemption (burn) or stablecoin payout. | | `transfers` | Transfer | Onchain transfer status transitions in either direction. | | `paymentIntents` | Payment intent | Stablecoin Payins intent lifecycle, including deposit-address assignment and timeline events. | | `payments` | Payment | Settled Stablecoin Payins payment, or a Stablecoin Payouts refund. | | `addressBookRecipients` | Address book recipient | Recipient review outcome and Travel Rule decision for Stablecoin Payouts. | | `externalEntities` | External entity | Core API for Institutions compliance outcome for an onboarded entity. | | `creditTransfers` | Credit transfer | Status transitions for a Settlement Advance or Line of Credit draw. | | `creditFees` | Credit fee | Fee accruals against a credit line. | | `creditRepayments` | Credit repayment | Matched fiat repayment or completed crypto repayment against a credit line. | | `approvalWorkflowTransferApproved` | Approval workflow event | A pending transfer was approved through the recipient approval workflow. | | `approvalWorkflowTransferRejected` | Approval workflow event | A pending transfer was rejected through the recipient approval workflow. | ### `wire` Lifecycle notifications for a linked wire bank account. Use these events to track the status of bank-account creation requests submitted through [`POST /v1/businessAccount/banks/wires`](/api-reference/circle-mint/account/create-business-wire-account). | Status | Meaning | | ---------- | -------------------------------------------------------------------- | | `pending` | Circle is reviewing the bank account. | | `complete` | The bank account is linked and can be used for deposits and payouts. | | `failed` | The bank account could not be linked. | ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "wire", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "wire": { "id": "8c33b3eb-67a4-4f3d-9f4e-2d8a4f1c2b6a", "status": "complete", "description": "WELLS FARGO BANK, NA ****0010", "trackingRef": "CIR2VKZ9G6", "fingerprint": "eb74e904-2c64-4f4d-9f54-9f1b8f7a2bb1", "billingDetails": { "name": "Satoshi Nakamoto", "city": "Boston", "country": "US", "line1": "100 Money Street", "district": "MA", "postalCode": "02201" }, "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:25:11.000Z" } } ``` ### `deposits` Fires when a fiat deposit (mint) settles to your Circle Mint balance. The notification carries the deposit `id`, the settled `amount`, the `source` (the linked wire bank account), and the destination wallet. | Status | Meaning | | ---------- | --------------------------------------------------------------- | | `pending` | Circle has received the wire and is processing the mint. | | `complete` | USDC or EURC has been credited to the destination wallet. | | `failed` | The deposit could not be completed and funds were not credited. | When the originating wire memo included a `customerExternalRef` matching the `EXT...` format (used by the Core API for Institutions), Circle echoes that value on the deposit so you can reconcile the credit to the originating client. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "deposits", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "deposit": { "id": "df3b8e5f-9579-4c1f-9fa9-deac7f4be55c", "status": "complete", "amount": { "amount": "1000.00", "currency": "USD" }, "fees": { "amount": "0.00", "currency": "USD" }, "source": { "id": "8c33b3eb-67a4-4f3d-9f4e-2d8a4f1c2b6a", "type": "wire" }, "destination": { "id": "1000038499", "type": "wallet" }, "customerExternalRef": "EXT0000000000000000A1", "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:26:02.000Z" } } ``` ### `payouts` Fires for fiat redemptions (burns) and stablecoin payouts. The payout resource carries the destination (a `wire` bank account or an `address_book` recipient), the gross `amount`, fees, and any error information. The most important fields to consume: * `id`: Unique payout identifier. * `destination`: Either a `wire` destination (fiat redemption) or an `address_book` destination (stablecoin payout). * `amount`: Gross amount of the payout, as a money object. * `toAmount`: Net amount delivered to the destination. Present only on stablecoin payouts. * `trackingRef`: Reference that appears on the bank statement. Present on fiat redemption (burn) payouts. * `sourceWalletId`: Identifier of the wallet funding the payout. * `fees`: Fees deducted from the source wallet, as a money object. * `networkFees`: Onchain network fees. Present only on stablecoin payouts. * `status`: `pending`, `complete`, or `failed`. * `errorCode`: Populated when `status` is `failed`. See [Stablecoin Payouts errors](/api-reference/circle-mint/error-codes#stablecoin-payouts-errors) for the catalog. * `riskEvaluation`: Risk decision and reason, populated for compliance-driven denials. | Status | Meaning | | ---------- | ------------------------------------------------------------------ | | `pending` | The payout is in progress. | | `complete` | Funds have been delivered to the destination. | | `failed` | The payout could not be completed. See `errorCode` for the reason. | ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "payouts", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "payout": { "id": "df3b8e5f-9579-4c1f-9fa9-deac7f4be55c", "sourceWalletId": "1000038499", "destination": { "id": "4d260293-d17c-4309-a8da-fa7850f95c10", "type": "address_book" }, "amount": { "amount": "100.00", "currency": "USD" }, "toAmount": { "amount": "100.00", "currency": "USD" }, "fees": { "amount": "0.50", "currency": "USD" }, "networkFees": { "amount": "0.10", "currency": "USD" }, "trackingRef": "CIR2VKZ9G6", "status": "complete", "riskEvaluation": { "decision": "approved", "reason": "1000" }, "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:26:02.000Z" } } ``` ### `transfers` Fires on every status transition for an onchain transfer, in either direction (Circle wallet to blockchain address, blockchain address to Circle wallet, or wallet to wallet). You receive one notification per transition, so a transfer that runs to completion produces multiple events. | Status | Meaning | | ---------- | -------------------------------------------------------------------- | | `pending` | The transfer has been submitted and is awaiting onchain broadcast. | | `running` | The transfer is broadcast and waiting for confirmations. | | `complete` | The transfer is confirmed onchain and settled. | | `failed` | The transfer could not be completed. See `errorCode` for the reason. | When `status` is `failed`, the transfer carries an `errorCode`; see [Transfer entity errors](/api-reference/circle-mint/error-codes#transfer-entity-errors) for the catalog. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "transfers", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "transfer": { "id": "0d46b642-3a5f-4071-a747-4053b7df2f99", "source": { "type": "wallet", "id": "1000038499" }, "destination": { "type": "blockchain", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "chain": "ARC" }, "amount": { "amount": "3.14", "currency": "USD" }, "transactionHash": "0x4cebf8f90c9243a23c77e4ae20df691469e4b933b295a73376292843968f7a63", "status": "complete", "riskEvaluation": { "decision": "approved", "reason": "1000" }, "createDate": "2026-01-15T18:23:44.000Z" } } ``` ### `paymentIntents` Fires on lifecycle changes for a Stablecoin Payins payment intent, including deposit-address assignment, settlement, expiry, refund, and failure. The payload includes the intent's full state—`paymentMethods[]` (the assigned deposit addresses), `timeline[]` (an ordered history of `status` and `context` transitions with timestamps), `amountPaid`, `amountRefunded`, `settlementCurrency`, and `fees[]`. The intent's lifecycle depends on `type`: * `continuous` intents stay at `active` after Circle assigns the deposit address and never advance to `complete`. To reconcile settled transfers against a continuous intent, listen to the `payments` topic or call [`GET /v1/payments`](/api-reference/circle-mint/payments/list-payments) with `paymentIntentId={id}`. * `transient` intents move through `created` → `pending` → `complete`, where the latest `timeline[]` entry carries a `context` of `paid`, `underpaid`, or `overpaid`. Terminal states are `expired`, `failed`, and `refunded`. For background on intent modes and the payin flow, see [How Stablecoin Payins and Payouts work](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work). ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "paymentIntents", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "paymentIntent": { "id": "5cb31987-66f1-4ce6-87ce-fb74dfe7c2dd", "type": "continuous", "amount": { "amount": "0.00", "currency": "USD" }, "amountPaid": { "amount": "0.00", "currency": "USD" }, "amountRefunded": { "amount": "0.00", "currency": "USD" }, "settlementCurrency": "USD", "paymentMethods": [ { "type": "blockchain", "chain": "ARC", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F" } ], "fees": [], "timeline": [ { "status": "active", "time": "2026-01-15T18:23:44.000Z" }, { "status": "created", "time": "2026-01-15T18:23:40.000Z" } ], "createDate": "2026-01-15T18:23:40.000Z", "updateDate": "2026-01-15T18:23:44.000Z" } } ``` ### `payments` Fires for inbound Stablecoin Payins settlements and for Stablecoin Payouts refunds. Both flows use the same `payments` resource; the `type` field discriminates between them: `payment` for an inbound settlement and `refund` for an outbound refund. | Status | Meaning | | ----------------- | ------------------------------------------------------------------------------ | | `pending` | The payment has been observed and is awaiting confirmations. | | `confirmed` | The payment has reached the required number of onchain confirmations. | | `paid` | The payment has settled to your Circle Mint balance. | | `failed` | The payment could not be settled. | | `action_required` | Manual intervention is required, such as a refund decision on an underpayment. | ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "payments", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "payment": { "id": "b9aef7d4-2eb4-4f4a-9b7f-71b3a4c9bb3b", "type": "payment", "status": "paid", "paymentIntentId": "5cb31987-66f1-4ce6-87ce-fb74dfe7c2dd", "amount": { "amount": "250.00", "currency": "USD" }, "feeAmount": { "amount": "0.50", "currency": "USD" }, "source": { "type": "blockchain", "chain": "ARC", "address": "0x6E1A4C16fAFC4ec1Aa1Dc6Fbe5cE9aB2B22B3F11" }, "depositAddress": { "chain": "ARC", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F" }, "transactionHash": "0x4cebf8f90c9243a23c77e4ae20df691469e4b933b295a73376292843968f7a63", "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:25:11.000Z" } } ``` ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "payments", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "payment": { "id": "c12ab9d6-3fc7-4ec1-92d4-58c1a4d5fae2", "type": "refund", "status": "paid", "paymentIntentId": "5cb31987-66f1-4ce6-87ce-fb74dfe7c2dd", "originalPaymentId": "b9aef7d4-2eb4-4f4a-9b7f-71b3a4c9bb3b", "amount": { "amount": "250.00", "currency": "USD" }, "settlementAmount": { "amount": "250.00", "currency": "USD" }, "destination": { "type": "blockchain", "chain": "ARC", "address": "0x6E1A4C16fAFC4ec1Aa1Dc6Fbe5cE9aB2B22B3F11" }, "transactionHash": "0x9fbe04f70cb1d5f5c12c93dcb2e21a8d6c3b8a4f93e1c01e2c3a4b5c6d7e8f90", "createDate": "2026-01-15T19:02:11.000Z", "updateDate": "2026-01-15T19:04:48.000Z" } } ``` ### `addressBookRecipients` Fires on lifecycle transitions for a Stablecoin Payouts recipient registered through the Address Book API. Use this topic to learn when a recipient is ready to receive payouts and to handle Travel Rule decisions on counterparties. | Status | Meaning | | ---------- | ----------------------------------------------------------- | | `pending` | Circle is reviewing the recipient. | | `inactive` | The recipient is in the delayed-withdrawals holding period. | | `active` | The recipient is ready to receive payouts. | | `denied` | The recipient failed review and cannot receive payouts. | For Travel Rule requirements and identity schemas that influence these decisions, see [Travel rule compliance](/circle-mint/references/travel-rule-compliance). ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "addressBookRecipients", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "addressBookRecipient": { "id": "4d260293-d17c-4309-a8da-fa7850f95c10", "status": "active", "chain": "ARC", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "nickname": "Vendor wallet", "metadata": { "nickname": "Vendor wallet", "type": "business" }, "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:26:02.000Z" } } ``` ### `externalEntities` Fires when Circle finishes its compliance review for an external entity onboarded through the Institutional API. The webhook carries the final `complianceState` decision; `walletId` is present only when the decision is `ACCEPTED`. | `complianceState` | Meaning | | ----------------- | -------------------------------------------------------------------------------- | | `PENDING` | The entity is under review. The accompanying `walletId` is unusable. | | `ACCEPTED` | The entity passed review. The accompanying `walletId` is provisioned and usable. | | `REJECTED` | The entity failed review and cannot operate through Circle Mint. | For background on the onboarding flow and how to use a provisioned `walletId`, see [Institutional API](/circle-mint/concepts/institutional-api) and [Manage institutional subaccounts](/circle-mint/quickstarts/manage-institutional-subaccounts). ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "externalEntities", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "externalEntity": { "id": "9a4d6e72-78aa-4b9d-bd84-2cc7c1a8c2d4", "complianceState": "ACCEPTED", "walletId": "212000", "businessName": "Example Trading Ltd.", "businessUniqueIdentifier": "TAX-001", "identifierIssuingCountryCode": "US", "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:30:00.000Z" } } ``` ### `creditTransfers` Fires when a credit draw changes status. The two Credit API products follow different lifecycles, so the meaningful status values depend on which product the credit line is scoped to. Settlement Advance status values: | Status | Meaning | | ---------------- | ------------------------------------------------------------------------------------ | | `funds_reserved` | Capacity is reserved against the credit line; the reservation expires in 30 minutes. | | `requested` | Wire proof has been submitted and Treasury is reviewing the request. | | `disbursed` | Treasury approved the advance and funds have landed. | | `paid` | The disbursed amount is fully repaid. | | `past_due` | The disbursed amount has not been fully repaid by its due date. | | `expired` | The reservation timed out before progressing to `requested`. | | `canceled` | You canceled the reservation before submitting wire proof. | | `rejected` | Treasury declined the request. | Line of Credit status values: | Status | Meaning | | ----------- | ------------------------------------------------------------------------------------ | | `requested` | The draw was created and is being processed for disbursement. | | `disbursed` | Funds have landed in your Mint wallet or at the verified Credit Express destination. | | `paid` | The disbursed amount is fully repaid. | | `past_due` | The disbursed amount has not been fully repaid by its due date. | | `rejected` | Circle declined the request. | When a transfer is configured with a Credit Express destination, the disbursement carries an additional onchain delivery status: | Credit Express destination status | Meaning | | --------------------------------- | ------------------------------------------- | | `pending` | The onchain delivery has not started. | | `initiated` | The onchain transaction has been broadcast. | | `complete` | The onchain delivery is confirmed. | | `failed` | The onchain delivery failed. | For background on the credit-line model and the two product lifecycles, see the [Credit API](/circle-mint/concepts/credit-api) concept. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "creditTransfers", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "creditTransfer": { "id": "c3a2f1e0-7e8c-4b3d-9b9b-1f4e2a7d4c11", "product": "lineOfCredit", "status": "disbursed", "amount": { "amount": "50000.00", "currency": "USD" }, "outstandingAmount": { "amount": "50000.00", "currency": "USD" }, "destination": { "type": "wallet", "id": "1000038499" }, "disbursedAt": "2026-01-15T18:23:44.000Z", "dueAt": "2026-01-22T18:23:44.000Z", "createDate": "2026-01-15T18:23:00.000Z", "updateDate": "2026-01-15T18:23:44.000Z" } } ``` ### `creditFees` Fires when a fee accrues against a credit line. Cadence matches the credit line's `feeCadence`: `daily` lines emit a fee notification every 24 hours and `hourly` Line of Credit lines emit one every hour. Each notification carries the accrued amount, the credit line it applies to, and the period it covers. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "creditFees", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "creditFee": { "id": "fee-1ef1f5cf-2b1b-4f12-aa6c-2c4f9b8fb3da", "creditLineId": "9c2c5f4d-1b6c-4a5f-9d0e-3a4b5c6d7e8f", "creditTransferId": "c3a2f1e0-7e8c-4b3d-9b9b-1f4e2a7d4c11", "type": "recurringFee", "amount": { "amount": "50.00", "currency": "USD" }, "feeCadence": "daily", "periodStart": "2026-01-15T18:23:44.000Z", "periodEnd": "2026-01-16T18:23:44.000Z", "createDate": "2026-01-16T18:23:44.000Z" } } ``` ### `creditRepayments` Fires when Circle matches an incoming fiat wire to a credit line or records a completed crypto repayment. The payload identifies the credit line, the repayment method (`fiat` or `crypto`), the matched amount, and the resulting outstanding balance. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "creditRepayments", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "creditRepayment": { "id": "rep-3c8d9e0f-1a2b-4c5d-6e7f-8a9b0c1d2e3f", "creditLineId": "9c2c5f4d-1b6c-4a5f-9d0e-3a4b5c6d7e8f", "creditTransferId": "c3a2f1e0-7e8c-4b3d-9b9b-1f4e2a7d4c11", "type": "fiat", "status": "complete", "amount": { "amount": "50050.00", "currency": "USD" }, "outstandingAmount": { "amount": "0.00", "currency": "USD" }, "createDate": "2026-01-22T16:00:00.000Z", "updateDate": "2026-01-22T16:05:00.000Z" } } ``` ### `approvalWorkflowTransferApproved` and `approvalWorkflowTransferRejected` Recipient approval workflow events. Some regions require a separate approval step before a transfer can proceed; France and Singapore are two examples. Pending transfers are routed to an approver. The decision is delivered on `approvalWorkflowTransferApproved` when the proposal is approved or on `approvalWorkflowTransferRejected` when it is rejected. The payload carries the proposal's `transferStatus`, `proposalStatus`, the originating `idempotencyKey`, and the `transferId` of the affected transfer. ```json theme={null} { "clientId": "00000000-0000-0000-0000-000000000001", "notificationType": "approvalWorkflowTransferApproved", "version": 1, "customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" }, "approvalWorkflow": { "transferId": "0d46b642-3a5f-4071-a747-4053b7df2f99", "transferStatus": "pending", "proposalStatus": "approved", "idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "createDate": "2026-01-15T18:23:44.000Z", "updateDate": "2026-01-15T18:28:00.000Z" } } ``` # How-to: Rotate an mTLS API key Source: https://developers.circle.com/circle-mint/rotate-mtls-api-key Rotate your API key for an mTLS-enabled Circle Mint entity before the mandatory 180-day expiration. API keys on mTLS-enabled entities carry a maximum lifetime of 180 days, whether you enabled mTLS optionally or under MiCA. API keys that exceed this limit are automatically invalidated. This guide walks you through generating a replacement key and rotating your integration with zero downtime. ## Prerequisites Before you begin, ensure that you've: * [Configured mTLS on your entity](/circle-mint/set-up-mtls-authentication) and have a working integration. * Configured access to the [Mint Console](https://app.circle.com) as an Administrator with multi-factor authentication (MFA). ## Steps 1. Sign in to the [Mint Console](https://app.circle.com). 2. Complete the MFA challenge. MFA is required for all API key operations on mTLS-enabled entities. 3. Generate a new API key and store it securely. Generate the new key well in advance of the 180-day expiration. After 180 days, the old key is automatically invalidated and can no longer authenticate requests. Replace the `Authorization: Bearer` header value in your integration with the new API key. For example, update the environment variable or secrets manager entry that stores your key: ```text theme={null} YOUR_API_KEY=your-new-api-key ``` Send a test request using the new API key and your existing client certificate to confirm the new key works. The example below uses `api-eu.circle.com` (the MiCA-regulated hostname). If you enabled mTLS optionally, substitute `api.circle.com`: ```bash theme={null} curl -v --cert /path/to/client-fullchain.pem \ --key /path/to/client-key.pem \ --request GET \ --url https://api-eu.circle.com/v1/businessAccount/balances \ --header "Authorization: Bearer ${YOUR_API_KEY}" ``` Look for `SSL connection using TLSv1.3` in the verbose output and confirm you receive a successful response. If you receive a `401` error, verify that you copied the new key correctly. After you confirm the new key is working in your integration: 1. Sign in to the [Mint Console](https://app.circle.com). 2. Complete the MFA challenge. 3. Revoke the old API key. Do not revoke the old key until you have verified that the new key works. Revoking the old key is irreversible. # How-to: Rotate an mTLS client certificate Source: https://developers.circle.com/circle-mint/rotate-mtls-certificate Rotate the client certificate for an mTLS-enabled Circle Mint entity before it expires. Client certificates issued by Circle are valid for 365 days. This guide walks you through generating a new key pair and certificate signing request (CSR), obtaining a renewed certificate from Circle, and rotating your integration with zero downtime. ## Prerequisites Before you begin, ensure that you've: * [Configured mTLS on your entity](/circle-mint/set-up-mtls-authentication) and have a working integration. * Installed OpenSSL 1.1.1 or later on your machine for key pair and CSR operations. ## Steps Generate a new Elliptic Curve Digital Signature Algorithm (ECDSA) P-256 key pair. Circle accepts only ECDSA P-256 keys. RSA keys and other curves are rejected. ```bash theme={null} openssl ecparam -genkey -name prime256v1 -noout -out new-client-key.pem ``` Keep your private key (`new-client-key.pem`) secure and never share it with Circle or any third party. Only the CSR, which contains your public key, is submitted. Generate a PKCS#10 CSR from your new key pair: ```bash theme={null} openssl req -new -key new-client-key.pem -out new-client.csr \ -subj "/CN=/O=" ``` Start this process at least two weeks before your current certificate expires to allow time for Circle to process the request and for you to test the new certificate. Provide your entity ID and new CSR file (`new-client.csr`) to [Circle Support](https://support.circle.com) or your Circle account manager, and request a renewed client certificate. Circle issues a renewed certificate from its private certificate authority (CA) and delivers a single file, `new-client-fullchain.pem`, through a secure, out-of-band channel. It contains your renewed client certificate followed by the CA certificate chain. Confirm that the renewed certificate matches your new private key by comparing the public key hashes: ```bash theme={null} openssl ec -in new-client-key.pem -pubout 2>/dev/null | openssl sha256 openssl x509 -in new-client-fullchain.pem -pubkey -noout 2>/dev/null | openssl sha256 ``` Both commands must return the same SHA-256 hash. If they differ, the certificate and key do not form a valid pair. Point your integration to the new certificate and key files. Update the `--cert` and `--key` paths (or the equivalent configuration in your HTTP client) to reference the new PEM files. Send a test request using the new certificate and your current API key. The example below uses `api-eu.circle.com` (the MiCA-regulated hostname). If you enabled mTLS optionally, substitute `api.circle.com`: ```bash theme={null} curl -v --cert /path/to/new-client-fullchain.pem \ --key /path/to/new-client-key.pem \ --request GET \ --url https://api-eu.circle.com/v1/businessAccount/balances \ --header "Authorization: Bearer ${YOUR_API_KEY}" ``` Look for `SSL connection using TLSv1.3` in the verbose output and confirm you receive a successful response. After you verify that the new certificate works in your integration: 1. Remove the old certificate and key files from your servers. 2. Securely delete the old private key material. # How-to: Set up mTLS authentication Source: https://developers.circle.com/circle-mint/set-up-mtls-authentication Configure mutual TLS authentication for Circle Mint API calls using a client certificate issued by Circle and an API key. This guide walks you through configuring mutual TLS (mTLS) for Circle Mint API calls by pairing a client certificate issued by Circle with a new API key. You generate a key pair and certificate signing request (CSR) locally, submit the CSR to Circle, and Circle issues your signed certificate. After completing these steps, your client presents the certificate during the TLS handshake and an API key in the HTTP header on every request. Entities operating in an EU/EEA member state under the Markets in Crypto-Assets (MiCA) regulation must use mTLS. Any other Circle Mint customer with an active API key can opt in to mTLS for extra security. Until Circle enables mTLS on your entity, standard API key authentication continues to work with no changes. ## Prerequisites Before you begin, ensure that you've: * Contacted [Circle Support](https://support.circle.com) or your Circle account manager to request that Circle enable mTLS on your entity. * Installed OpenSSL 1.1.1 or later on your machine for key pair and CSR operations. * Configured access to the [Mint Console](https://app.circle.com) with multi-factor authentication (MFA). For background on mTLS, see [How mTLS authentication works](/circle-mint/mtls-authentication). ## Steps Generate an Elliptic Curve Digital Signature Algorithm (ECDSA) P-256 key pair. Circle accepts only ECDSA P-256 keys. RSA keys and other curves are rejected. ```bash theme={null} openssl ecparam -genkey -name prime256v1 -noout -out client-key.pem ``` Keep your private key (`client-key.pem`) secure and never share it with Circle or any third party. Only the CSR, which contains your public key, is submitted. Generate a PKCS#10 CSR from your key pair: ```bash theme={null} openssl req -new -key client-key.pem -out client.csr \ -subj "/CN=/O=" ``` Replace `` and `` with your entity's legal name. These values help Circle identify your request. Circle assigns the final certificate fields during issuance. Before submitting, confirm the CSR uses the correct key type: ```bash theme={null} openssl req -in client.csr -noout -text ``` The output must show a public key algorithm of `id-ecPublicKey` and an ASN1 OID of `prime256v1`. Provide the following to [Circle Support](https://support.circle.com) or your Circle account manager: * Your entity ID, found in the Mint Console under **Settings**. * Your CSR file (`client.csr`). * A request to enable mTLS on your entity. Circle validates your CSR and issues a signed client certificate from its private certificate authority (CA). Circle delivers a single file, `client-fullchain.pem`, through a secure, out-of-band channel. It contains your signed client certificate, valid for 365 days, followed by the CA certificate chain. Confirm that the issued certificate matches your private key by comparing the public key hashes: ```bash theme={null} openssl ec -in client-key.pem -pubout 2>/dev/null | openssl sha256 openssl x509 -in client-fullchain.pem -pubkey -noout 2>/dev/null | openssl sha256 ``` Both commands must return the same SHA-256 hash. If they differ, the certificate and key do not form a valid pair. When Circle enables mTLS on your entity, all existing API keys are revoked immediately. You must generate a new key before you can make API calls. 1. Sign in to the [Mint Console](https://app.circle.com). 2. Complete the MFA challenge. MFA is required for all API key operations on mTLS-enabled entities. 3. Generate a new API key and store it securely. API keys on mTLS-enabled entities have a maximum lifetime of 180 days. Plan to rotate your key before it expires. See [How-to: Rotate an mTLS API key](/circle-mint/rotate-mtls-api-key) for the rotation procedure. Call the hostname that matches how you enabled mTLS: * **MiCA-regulated**: Use the regional EU hostname `api-eu.circle.com` for all API traffic. Requests sent to `api.circle.com` are rejected. * **Optional (non-MiCA)**: Continue to use the standard hostname `api.circle.com`. Your endpoint URLs are unchanged. The examples in this step use `api-eu.circle.com`. If you enabled mTLS optionally, substitute `api.circle.com`. Combine your client certificate and API key in a single request. The following example retrieves your account balances: ```bash theme={null} curl --cert /path/to/client-fullchain.pem \ --key /path/to/client-key.pem \ --request GET \ --url https://api-eu.circle.com/v1/businessAccount/balances \ --header "Authorization: Bearer ${YOUR_API_KEY}" ``` To confirm that the TLS handshake is succeeding and that TLS 1.3 is in use, add the `-v` (verbose) flag: ```bash theme={null} curl -v --cert /path/to/client-fullchain.pem \ --key /path/to/client-key.pem \ --request GET \ --url https://api-eu.circle.com/v1/businessAccount/balances \ --header "Authorization: Bearer ${YOUR_API_KEY}" ``` Look for `SSL connection using TLSv1.3` in the verbose output. Circle supports TLS 1.2 and TLS 1.3, but TLS 1.3 is recommended. Complete the two-way trust relationship by validating the server certificate Circle presents during the handshake. How you validate it depends on why you enabled mTLS. If you enabled mTLS **under MiCA**, Circle presents a Qualified Website Authentication Certificate (QWAC) at `api-eu.circle.com`. Choose one of the following approaches to validate it. **Option A: Use a payment aggregator (recommended)** Delegate certificate validation to a payment aggregator. The aggregator handles trust chain verification, revocation checks, and PSD2 compliance validation on your behalf. This approach reduces integration complexity and ongoing maintenance. **Option B: Validate directly against EU Trusted Lists** If you validate Circle's server certificate directly, your implementation must: 1. **Verify the certificate chain** against the root certificate authorities (CAs) published on the [EU Trusted Lists](https://eidas.ec.europa.eu/efda/tl-browser/). 2. **Confirm PSD2 QcStatements** are present in the certificate. These statements attest that the certificate is a QWAC issued for PSD2 purposes. 3. **Check OCSP revocation status** to ensure the certificate has not been revoked. 4. **Verify the Organization Identifier and NCA** in the certificate match Circle's registration details. If you enabled mTLS **optionally (non-MiCA)**, Circle presents its standard server certificate at `api.circle.com`. Validate it the same way you validate any HTTPS connection, using the CA certificate chain contained in `client-fullchain.pem`. Neither QWAC option applies to you. ## Troubleshooting The following table lists common errors and their causes. | Error | Cause | Resolution | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` "Invalid credentials" | The API key is invalid, expired, or was revoked when mTLS was enabled. | Generate a new API key from the [Mint Console](https://app.circle.com) with MFA. | | `403` "Valid mTLS client certificate required" | No client certificate was presented, or the certificate does not match the entity. | Verify that you pass `--cert` and `--key` in your request and that you use the certificate Circle issued for your entity. | | TLS handshake failure | The certificate and private key do not match, the certificate format is unsupported, or the certificate is expired. | Compare the certificate and key public key hashes to verify the cert/key pair, as shown when you received your certificate. Confirm the certificate has not expired. | | Connection rejected or timeout (MiCA) | The request was sent to `api.circle.com` instead of the regional EU hostname. | If you enabled mTLS under MiCA, update your integration to call `api-eu.circle.com` for every endpoint. | # 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`](/api-reference/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`](/api-reference/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`](/api-reference/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]; } ``` # 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 a webhook endpoint](/api-reference/webhook-endpoints), and optionally watch the following video about Webhook Configurations.