# 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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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.
## 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_202510062025-10-07T14:30:45Ze0549c6e-c80e-4e5f-95ee-c66f7d1be455EntityId0455_1000123456_USD_202510061000123456USDOPBD1750000.00CRDT
2025-10-06T00:00:00Z
CLBD1775000.00CRDT
2025-10-06T23:59:59Z
25000.00CRDTBOOK2025-10-06T08:15:23ZMintwireCircleTransactionData550e8400-e29b-41d4-a716-446655440000660e9511-f3ac-52e5-b827-557766551111USDC
```
## 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 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.
## Customized Webhook Notifications
Customized webhook notifications enable you to listen to a restricted set of
notifications. To create a webhook for smart contract events only select Limit
to specific events when creating your webhook in the Circle Console, then choose
contracts.EventLog.
## The Event Log Notification
When a monitored event occurs, the webhook will receive a notification via POST
with the body formatted as follows: Notification
```json JSON theme={null}
{
// A unique identifier for the subscription that received the notification.
"subscriptionId": "c881d120-7692-4ae6-bab0-acfa8c9596c1",
// A unique identifier for the notification itself.
"notificationId": "aff457c5-2649-4cdb-a51e-84b33fdfebc9",
// The type of notification; indicates an event log from a smart contract.
"notificationType": "contracts.eventLog",
// An object that contains details about the event log notification.
"notification": {
// The address of the smart contract that emitted the event.
"contractAddress": "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The specific blockchain network where the event occurred.
"blockchain": "ARB-SEPOLIA",
// A hash to identify the on-chain transaction that caused the event.
"txHash": "0xdc8c0ac07ca02f87754e931b729d9426a5bd7f6b062fbb40e58c8a7942992a7a",
// A hash representing the user operation that triggered the event, if applicable.
"userOpHash": "0xbaae33f07aa4877f5a2e9f2a90c5cc5c5006281043696ebaa7f887914058ead0",
// The name and parameter types of the emitted event.
"eventName": "Transfer(address,address,uint256)",
// An array containing indexed parameters of the event. Each entry is a hashed representation of the parameter.
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000009d7fb3144729fff819fc1aa812d0a261ed8c8676"
],
// The raw data associated with the event, consisting of non-indexed parameters as a concatenated hexadecimal string.
"data": "0x00000000000000000000000000000000000000000000000000005af3107a3fff"
},
// The UTC timestamp of when the notification was generated, formatted to ISO 8601.
"timestamp": "2024-10-21T18:44:07.614649603Z",
// Indicates the version of the notification format.
"version": 2
}
```
### Step 2. Import Your Smart Contract
Before creating an event monitor, import your smart contract into the Contracts
system. This ensures the Contracts platform is aware of your contract and can
manage event monitors effectively:
```javascript javascript theme={null}
const importResponse = await circleContractSdk.importContract({
// The address of the smart contract to be imported.
address: "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The blockchain from which the contract is being imported.
blockchain: "ARB-SEPOLIA",
// A unique key provided to ensure idempotency of the request. It prevents duplicate imports for the same contract if a retry occurs.
idempotencyKey: "50d64a5e-6b2e-47ea-aa14-13feab9376e9",
// A human-readable name for the contract being imported.
name: "MyToken",
// A brief description of the smart contract.
description: "My ERC-20 Token Contract",
});
```
If you attempt to create an event monitor on an un-imported smart contract, you
will receive the following error message:
```json JSON theme={null}
{
"code": 175001,
"message": "contract not found"
}
```
For more information on importing a contract, see the Import a contract API
endpoint.
## Step 3. Create an Event Monitor
Event Monitors listen for configured events on the blockchain and trigger
notifications to your webhook endpoint when those events occur. Circle will also
save all emitted events under that monitor, allowing you to retrieve historical
data later. For more details, see Step 4. Fetch Event History.
### Finding and Using the Event Signature
An event signature uniquely identifies an event within a smart contract and is
derived from the event's name and parameter types using the keccak-256 hashing
function. For readability, event monitors are created using the human-readable
event signature, not the hash.
For example, the Transfer event in the ERC-20 interface is defined as:
```javascript JavaScript theme={null}
event Transfer(address indexed from, address indexed to, uint256 value);
```
The human-readable event signature is formatted as:
'Transfer(address,address,uint256)'.
**Note:** Spaces are not allowed in the event string signature.
Create an event monitor with the following code:
```javascript JavaScript theme={null}
const monitorResponse = await circleContractSdk.createEventMonitor({
// The specific blockchain network where the event monitor will be created.
blockchain: "ARB-SEPOLIA",
// The address of the smart contract for which the event monitor is being created.
contractAddress: "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The signature of the event to monitor, defining the event type and parameters being tracked.
eventSignature: "Transfer(address,address,uint256)",
// A unique key provided to ensure idempotency of the request, preventing duplicate event monitors from being created.
idempotencyKey: "f80fcf44-bbb1-4336-870a-f1802ad98e0f",
});
```
The response will include details about the event monitor:
```json JSON theme={null}
{
"data": {
"eventMonitor": {
// A unique identifier for the event monitor.
"id": "01929bc2-aab3-76a2-b3fc-c99fc94b9218",
// The address of the smart contract being monitored.
"contractAddress": "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The specific blockchain where the event monitor is set up.
"blockchain": "ARB-SEPOLIA",
// The signature of the event being monitored, indicating the type and parameters.
"eventSignature": "Transfer(address,address,uint256)",
// The hash of the event signature used for efficient identification.
"eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
// A boolean indicating whether the event monitor is currently enabled.
"isEnabled": true,
// The date and time when the event monitor was created, in ISO 8601 format.
"createDate": "2024-10-17T18:34:39.1558Z",
// The date and time when the event monitor was last updated, in ISO 8601 format.
"updateDate": "2024-10-17T18:34:39.1558Z"
}
}
}
```
If the event signature you are creating a monitor for does not exist, you will
receive the following error message:
```json JSON theme={null}
{
"code": 175303,
"message": "The specified event signature does not exist"
}
```
**Note:** Please ensure the event signature exists if you are using an
unverified contract as Circle cannot verify event existence for unverified
contracts.
After creating an event monitor, you can view, disable, and delete your
monitors. For more information, see the
[Event Monitoring APIs](/api-reference/contracts/smart-contract-platform/get-event-monitors).
### Step 4. Fetch Event History
To retrieve historical events that match your monitors, utilize the event
history feature. This functionality is essential for doing analytics, auditing,
or reconstructing the application state based on past events:
```javascript JavaScript theme={null}
const eventHistoryResponse = await circleContractSdk.getEventHistory({
// The address of the smart contract for which event history is being fetched.
contractAddress: "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The specific blockchain network where the contract is deployed.
blockchain: "ARB-SEPOLIA",
// The signature of the event to fetch historical occurrences.
eventSignature: "Transfer(address,address,uint256)",
});
```
The data returned will mirror the information in your Webhook Logs in the
[Circle Console](https://console.circle.com/):
```json JSON theme={null}
{
"data": {
"eventLogs": [
{
// A unique identifier for the event log.
"id": "0192b064-c71d-7642-8882-aadc8f5df01a",
// The blockchain network where the event occurred.
"blockchain": "ARB-SEPOLIA",
// The transaction hash associated with the event log.
"txHash": "0xdc8c0ac07ca02f87754e931b729d9426a5bd7f6b062fbb40e58c8a7942992a7a",
// The index of the log entry within the transaction.
"logIndex": "1",
// The hash of the block containing the transaction.
"blockHash": "0x842dfd3c9339008d311c78154368f0f11f91801d5f40d7306745576aa9c246f7",
// The height of the block in which the event occurred.
"blockHeight": 90526683,
// The address of the smart contract that emitted the event.
"contractAddress": "0x6bc50ff08414717f000431558c0b585332c2a53d",
// The signature of the event that was emitted.
"eventSignature": "Transfer(address,address,uint256)",
// The hash of the event signature for efficient identification.
"eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
// An array of topics indexed within the event log for filtering.
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000009d7fb3144729fff819fc1aa812d0a261ed8c8676"
],
// Raw data associated with the event, representing non-indexed parameters.
"data": "0x00000000000000000000000000000000000000000000000000005af3107a3fff",
// A hash representing the user operation that triggered the event, when applicable.
"userOpHash": "0xbaae33f07aa4877f5a2e9f2a90c5cc5c5006281043696ebaa7f887914058ead0",
// The date and time when the first confirmation of the event occurred.
"firstConfirmDate": "2024-10-21T18:44:04Z"
}
]
}
}
```
**Note:**
Event history for a monitor will always be available; however, the resource will
stop being populated once the monitor is deleted.
For more information, see the
[Event Monitoring APIs](/api-reference/contracts/smart-contract-platform/get-event-monitors)
and join our
[Discord - Build on Circle](https://discord.com/invite/buildoncircle) channel.
# Quickstart: Interact with a smart contract
Source: https://developers.circle.com/contracts/scp-interact-smart-contract
In this guide, we will interact with a deployed smart contract using APIs. If
you haven't deployed a smart contract, follow the
[deploy a smart contract](/contracts/scp-deploy-smart-contract) tutorial.
## 1. Retrieve the Smart Contract's Functions
To fetch the new Contract object, make a `GET /contracts/{id}` request that
includes the `contractId` object returned from your deployment request. The
`functions` property returned in the successful request contains a list of all
of the functions on the contract:
```javascript Node.js SDK theme={null}
// Import and configure the developer-controlled wallet SDK
const {
initiateDeveloperControlledWalletsClient,
} = require("@circle-fin/developer-controlled-wallets");
const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({
apiKey: "",
entitySecret: "",
});
// Import and configure the smart contract SDK
const {
initiateSmartContractPlatformClient,
} = require("@circle-fin/smart-contract-platform");
const circleContractSdk = initiateSmartContractPlatformClient({
apiKey: "",
entitySecret: "",
});
const response = await circleContractSdk.getContract({
id: "0189db84-72b7-7fcc-832b-5bf886b9a0ef",
});
```
```coffeescript Python SDK theme={null}
from circle.web3 import smart_contract_platform, developer_controlled_wallets
from circle.web3 import utils
# Import and configure the developer-controlled wallet SDK
client = utils.init_developer_controlled_wallets_client(api_key=key, entity_secret=entitySecret)
# Import and configure the smart contract SDK
scpClient = utils.init_smart_contract_platform_client(api_key=key, entity_secret=entitySecret)
# create an api instance
api_instance = smart_contract_platform.ViewUpdateApi(scpClient)
try:
response = api_instance.get_contract(id='0189db84-72b7-7fcc-832b-5bf886b9a0ef')
print(response)
except smart_contract_platform.ApiException as e:
print("Exception when calling ViewUpdateApi->get_contract: %s\n" % e)
```
```curl cURL theme={null}
curl --request GET \
--url 'https://api.circle.com/v1/w3s/contracts/{id}' \
--header 'accept: application/json' \
--header 'authorization: Bearer '
```
```json JSON theme={null}
{
"data": {
"contract": {
"id": "0189db84-72b7-7fcc-832b-5bf886b9a0ef",
"entityId": "dcfa8149-98d8-4ed4-91fb-de7f3627b384",
"deploymentTransactionId": "4f5bfa38-c598-56a6-932e-8b5bbd3d5fc9",
"name": "First Contract Name",
"description": "First Contract Description",
"contractInputType": "BYTECODE",
"createDate": "2023-08-09T18:17:17Z",
"updateDate": "2023-08-09T18:17:17Z",
"archived": false,
"contractAddress": "0x1e124d7384cd34448ea5907bd0052a79355ab5eb",
"blockchain": "MATIC-AMOY",
"status": "COMPLETE",
"deployerAddress": "0x1bf9ad0cc2ad298c69a2995aa806ee832788218c",
"txHash": "0x241c4df6f08f9ed2b569c9f9b1cc48fb6074ffffaeee7552e716ce059161a743",
"abiJson": "[\n\t{\n\t\t\"inputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"constructor\"\n\t},\n\t{\n\t\t\"anonymous\": false,\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"owner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"approved\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"Approval\",\n\t\t\"type\": \"event\"\n\t},\n\t{\n\t\t\"anonymous\": false,\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"owner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"operator\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": false,\n\t\t\t\t\"internalType\": \"bool\",\n\t\t\t\t\"name\": \"approved\",\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"ApprovalForAll\",\n\t\t\"type\": \"event\"\n\t},\n\t{\n\t\t\"anonymous\": false,\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"previousOwner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"newOwner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"OwnershipTransferred\",\n\t\t\"type\": \"event\"\n\t},\n\t{\n\t\t\"anonymous\": false,\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"from\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"indexed\": true,\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"Transfer\",\n\t\t\"type\": \"event\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"approve\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"owner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"balanceOf\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"getApproved\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"owner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"operator\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"isApprovedForAll\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"bool\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [],\n\t\t\"name\": \"name\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"string\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [],\n\t\t\"name\": \"owner\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"ownerOf\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [],\n\t\t\"name\": \"renounceOwnership\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"safeMint\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"from\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"safeTransferFrom\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"from\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"bytes\",\n\t\t\t\t\"name\": \"data\",\n\t\t\t\t\"type\": \"bytes\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"safeTransferFrom\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"operator\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"bool\",\n\t\t\t\t\"name\": \"approved\",\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"setApprovalForAll\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"bytes4\",\n\t\t\t\t\"name\": \"interfaceId\",\n\t\t\t\t\"type\": \"bytes4\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"supportsInterface\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"bool\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"bool\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [],\n\t\t\"name\": \"symbol\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"string\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"tokenURI\",\n\t\t\"outputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"string\",\n\t\t\t\t\"name\": \"\",\n\t\t\t\t\"type\": \"string\"\n\t\t\t}\n\t\t],\n\t\t\"stateMutability\": \"view\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"from\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"to\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"internalType\": \"uint256\",\n\t\t\t\t\"name\": \"tokenId\",\n\t\t\t\t\"type\": \"uint256\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"transferFrom\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t},\n\t{\n\t\t\"inputs\": [\n\t\t\t{\n\t\t\t\t\"internalType\": \"address\",\n\t\t\t\t\"name\": \"newOwner\",\n\t\t\t\t\"type\": \"address\"\n\t\t\t}\n\t\t],\n\t\t\"name\": \"transferOwnership\",\n\t\t\"outputs\": [],\n\t\t\"stateMutability\": \"nonpayable\",\n\t\t\"type\": \"function\"\n\t}\n]",
"functions": [
{
"name": "approve",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "balanceOf",
"type": "function",
"inputs": [
{
"name": "owner",
"type": "address"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "uint256"
}
]
},
{
"name": "getApproved",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "address"
}
]
},
{
"name": "isApprovedForAll",
"type": "function",
"inputs": [
{
"name": "owner",
"type": "address"
},
{
"name": "operator",
"type": "address"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "bool"
}
]
},
{
"name": "name",
"type": "function",
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "string"
}
]
},
{
"name": "owner",
"type": "function",
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "address"
}
]
},
{
"name": "ownerOf",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "address"
}
]
},
{
"name": "renounceOwnership",
"type": "function",
"stateMutability": "nonpayable"
},
{
"name": "safeMint",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
},
{
"name": "data",
"type": "bytes"
}
],
"stateMutability": "nonpayable"
},
{
"name": "setApprovalForAll",
"type": "function",
"inputs": [
{
"name": "operator",
"type": "address"
},
{
"name": "approved",
"type": "bool"
}
],
"stateMutability": "nonpayable"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "bool"
}
]
},
{
"name": "symbol",
"type": "function",
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "string"
}
]
},
{
"name": "tokenURI",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "view",
"outputs": [
{
"name": "",
"type": "string"
}
]
},
{
"name": "transferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "transferOwnership",
"type": "function",
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"stateMutability": "nonpayable"
}
],
"events": [
{
"name": "Approval",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true
},
{
"name": "approved",
"type": "address",
"indexed": true
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true
}
],
"anonymous": false
},
{
"name": "ApprovalForAll",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true
},
{
"name": "operator",
"type": "address",
"indexed": true
},
{
"name": "approved",
"type": "bool",
"indexed": false
}
],
"anonymous": false
},
{
"name": "OwnershipTransferred",
"type": "event",
"inputs": [
{
"name": "previousOwner",
"type": "address",
"indexed": true
},
{
"name": "newOwner",
"type": "address",
"indexed": true
}
],
"anonymous": false
},
{
"name": "Transfer",
"type": "event",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true
},
{
"name": "to",
"type": "address",
"indexed": true
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true
}
],
"anonymous": false
}
],
"verificationStatus": "UNVERIFIED"
}
}
```
**Note:** Smart contract functions are defined using the Application Binary
Interface (ABI). Some important ABI properties are:
1. `name`: The function name. This is used in the `abiFunctionSignature` to
select which function to call when reading from or executing a function on a
smart contract.
2. `inputs`: The names and types of the arguments to the function. These
parameters are passed to the `contracts/read` or
`developer/transactions/contractExecution` APIs via the `abiParameters`
property.
3. `outputs`: The output values when the function returns. Some functions do not
return anything.
4. `stateMutability`: Defines how the function interacts with the blockchain.
1. `view` and `pure` functions do not change the state of the blockchain, so
these are `read` functions.
2. `nonpayable` and `payable` functions update the state of the blockchain,
so these are `execute` functions. `nonpayable` is the default for a
function if no `stateMutability` property exists.
## 2. Contract Query Function
Once you have selected a read function from the list, you can send an API
request to `POST /contracts/query`. The example below is making a request to the
`owner()` function to check the current owner of the contract
**Notes:**
* The `abiFunctionSignature` is derived from the function name and the
parenthesized list of parameter types. For instance, the
`abiFunctionSignature` for a function named `testFunction` that takes an
`address` and an `uint256` would be `testFunction(address,uint256)`.
* Read interactions *do not* require gas on the blockchain, so you are not
required to a specify a wallet.
* A shortened `abiJson`, containing only the part of the ABI describing the
`owner` function of the contract, was used to demonstrate, but ordinarily, the
entire ABI would be included in the request.
```javascript Node.js SDK theme={null}
response = await client.queryContract({
abiFunctionSignature: "owner()",
address: "0x1e124d7384cd34448ea5907bd0052a79355ab5eb",
blockchain: "MATIC-AMOY",
abiJson:
'[{"inputs": [],"name": "owner","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"}]',
});
```
```coffeescript Python SDK theme={null}
# create an api instance
api_instance = smart_contract_platform.InteractApi(scpClient)
try:
request = smart_contract_platform.ReadContractStateRequest.from_dict({
"abiFunctionSignature": "owner()",
"address": "0x1e124d7384cd34448ea5907bd0052a79355ab5eb",
"blockchain": "MATIC-AMOY",
"abiJson": "[{\"inputs\": [],\"name\": \"owner\",\"outputs\": [{\"internalType\": \"address\",\"name\": \"\",\"type\": \"address\"}],\"stateMutability\": \"view\",\"type\": \"function\"}]"
})
response = api_instance.query_contract(request)
except smart_contract_platform.ApiException as e:
print("Exception when calling InteractApi->read_contract: %s\n" % e)
```
```curl Bash theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/contracts/query \
--header "Authorization: Bearer $API_KEY" \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{
"abiFunctionSignature": "owner()",
"address": "0x1e124d7384cd34448ea5907bd0052a79355ab5eb",
"blockchain": "MATIC-AMOY",
"abiJson": "[{\"inputs\": [],\"name\": \"owner\",\"outputs\": [{\"internalType\": \"address\",\"name\": \"\",\"type\": \"address\"}],\"stateMutability\": \"view\",\"type\": \"function\"}]"
}'
```
```json Response Body theme={null}
{
"data": {
"outputData": "0x000000000000000000000000D4D2DB6B4e42Db2c2C0797F009942b35e466DB29",
"outputValues": ["0xD4D2DB6B4e42Db2c2C0797F009942b35e466DB29"]
}
}
```
## 3. Contract Execution Function
Once you have chosen an execute function you can make a request to
`POST developer/transactions/contractExecution` or
`user/transactions/contractExecution`, for developer-controlled and
user-controlled wallets, respectively. The example below uses the
developer-controlled endpoint to call the `safeMint()` function to mint an NFT.
The function takes in two parameters: the `to` address and the assigned
`tokenId` for the NFT. The `abiFunctionSignature` is
`safeMint(address, uint256)`.
* **Note:** The `safeMint` function must be called by the `owner` of the
contract. We specified this function modifier when generating the contract
earlier.
* **Note:** Write functions *do* require gas on the blockchain, so a `walletId`
must be specified in the request.
```javascript Node.js SDK theme={null}
const response = await circleDeveloperSdk.createContractExecutionTransaction({
walletId: "ce714f5b-0d8e-4062-9454-61aa1154869b",
contractAddress: "0x2f3A40A3db8a7e3D09B0adfEfbCe4f6F81927557",
abiFunctionSignature: "safeMint(address, uint256)",
abiParameters: ["0x6E5eAf34c73D1CD0be4e24f923b97CF38e10d1f3", 1],
fee: {
type: "level",
config: {
feeLevel: "MEDIUM",
},
},
});
```
```coffeescript Python SDK theme={null}
# create an api instance
api_instance = developer_controlled_wallets.TransactionsApi(client)
try:
request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({
"walletId": 'ce714f5b-0d8e-4062-9454-61aa1154869b',
"contractAddress": '0x2f3A40A3db8a7e3D09B0adfEfbCe4f6F81927557',
"abiFunctionSignature": 'safeMint(address, uint256)',
"abiParameters": ['0x6E5eAf34c73D1CD0be4e24f923b97CF38e10d1f3', 1],
"feeLevel": 'MEDIUM'
})
response = api_instance.create_developer_transaction_contract_execution(request)
print(response)
except developer_controlled_wallets.ApiException as e:
print("Exception when calling TransactionsApi->create_developer_transaction_contract_execution: %s\n" % e)
```
```curl Bash theme={null}
curl --request POST \
--url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'authorization: Bearer ' \
--data '
{
"idempotencyKey": "d6d56c67-9d8b-4e87-a618-1c44fa04a495",
"walletId": "ce714f5b-0d8e-4062-9454-61aa1154869b",
"contractAddress": "0x1e124d7384cd34448ea5907bd0052a79355ab5eb",
"abiFunctionSignature": "safeMint(address, uint256)",
"abiParameters": [
"0x6E5eAf34c73D1CD0be4e24f923b97CF38e10d1f3",
1
],
"feeLevel": "MEDIUM",
"entitySecretCiphertext": "N8sYP20DJvujfnlSM4qirGXGN8Pr+FjgMWN7zHc96EiSJnPxoGviniZlNaLT..."
}
```
```json Response Body theme={null}
{
"data": {
"transactionId": "84cb1754-f236-4bfc-8562-70fa6ee168c8"
}
}
```
# Contracts templates
Source: https://developers.circle.com/contracts/scp-templates-overview
Deploy pre-audited smart contracts without writing Solidity code.
Smart contract templates are audited contracts that you can deploy using the
Contracts API. Each template uses a standard token format and has built-in
access control. You can deploy contracts in minutes without writing Solidity
code.
## Available templates
The table below lists all templates. Select a template to view its settings and
functions.
| Template | Standard | Template ID | Use cases |
| ---------------------------------------------- | -------- | -------------------------------------- | ------------------------------------------------------- |
| [Token](/contracts/erc-20-token) | ERC-20 | `a1b74add-23e0-4712-88d1-6b3009e85a86` | Fungible tokens, loyalty points, governance tokens |
| [NFT](/contracts/erc-721-nft) | ERC-721 | `76b83278-50e2-4006-8b63-5b1a2a814533` | Digital collectibles, gaming assets, tokenized assets |
| [Multi-Token](/contracts/erc-1155-multi-token) | ERC-1155 | `aea21da6-0aa2-4971-9a1a-5098842b1248` | Mixed fungible and non-fungible tokens, batch transfers |
| [Airdrop](/contracts/airdrop) | N/A | `13e322f2-18dc-4f57-8eed-4bddfc50f85e` | Bulk token distribution to multiple recipients |
## Benefits of templates
Using templates instead of custom contracts has several benefits:
* **Security**: Third-party firms audit each template. This lowers the risk of
common exploits.
* **Lower gas costs**: Templates use patterns that reduce transaction fees for
you and your users.
* **Standard compliance**: Templates follow ERC standards. This ensures they
work with wallets, marketplaces, and other blockchain tools.
* **Faster deployment**: Deploy contracts in minutes.
## Deploy a template
To deploy a template, send a `POST` request to the
[`/templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
endpoint with the template ID and your settings. For a full guide, see
[Deploy a smart contract](/contracts/scp-deploy-smart-contract).
# Circle Contracts supported blockchains
Source: https://developers.circle.com/contracts/supported-blockchains
Mainnet and testnet blockchains supported by Circle Contracts for smart contract deployment and management.
Circle Contracts is available on the following blockchains:
## Mainnet
| Blockchain |
| ----------- |
| Arbitrum |
| Avalanche |
| Base |
| Ethereum |
| Monad |
| OP Mainnet |
| Polygon PoS |
| Unichain |
## Testnet
| Blockchain |
| ---------------- |
| Arbitrum Sepolia |
| Arc Testnet |
| Avalanche Fuji |
| Base Sepolia |
| Ethereum Sepolia |
| Monad Testnet |
| OP Sepolia |
| Polygon PoS Amoy |
| Unichain Sepolia |
# Circle Payments Network
Source: https://developers.circle.com/cpn
Move and settle money globally through Circle Payments Network, a stablecoin payments network for accepting, converting, moving, and settling funds with USDC.
Circle Payments Network (CPN) is Circle's stablecoin payments network for global
money movement. Built and powered by Circle, the issuer of USDC, it gives
financial institutions and enterprises a single integration to accept, convert,
move, and settle funds.
Move money near-instantly, 24/7, on programmable stablecoin rails. Each payment
product handles a specific job, so you can pick what you need today and add more
as your business grows.
## Operating modes
Circle builds and powers every CPN payment. You choose who operates the payment
lifecycle, and you can change modes without re-integrating as your business
grows. The two modes differ in who holds the stablecoins and who owns licensing,
custody, and compliance.
* **Self-managed**: You operate your own stablecoin lifecycle, including
custody, payouts, and compliance. Circle provides the rails, APIs, and
routing. Choose this mode when you hold stablecoins directly and want control
over the flow of funds.
* **Managed**: Circle handles licensing, custody, compliance, treasury, and
settlement on your behalf. Choose this mode when you want stablecoin payments
without holding digital assets or building onchain systems.
## Products
Send cross-border fiat payouts through a network of payout partners. Request
real-time FX quotes, settle in USDC onchain, and let partners deliver local
fiat to the receiver.
Send USDC or EURC to third-party wallets with Stablecoin Payouts, and accept
it from third parties onchain with Stablecoin Payins. Both support Address
Book and Travel Rule data.
Offer stablecoin payins, payouts, and digital asset accounts to your
end-customers without holding digital assets. Circle runs custody,
compliance, and onchain activity while you stay in fiat at the edge.
## Choose a product
The following table maps each product to its operating mode and primary use
case.
| Product | Operating mode | Use it to |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------- |
| Fiat Payouts | Self-managed | Settle cross-border payments in USDC and deliver local fiat to receivers through payout partners. |
| Stablecoin Payouts | Self-managed | Send USDC or EURC to third-party wallets you register and approve. |
| Stablecoin Payins | Self-managed | Accept USDC or EURC from third parties into a deposit address. |
| Managed Payments | Managed | Operate fully in fiat while Circle powers stablecoin payments behind the scenes. |
## Get started
Request a quote, create a payment, complete the onchain USDC transfer, and
track status as an Originating Financial Institution.
Register a recipient in the Address Book and send USDC or EURC onchain to a
third party.
Create a payment intent, share the deposit address, and confirm the payment
when it settles.
# CPN API changes and compatibility
Source: https://developers.circle.com/cpn/concepts/api/api-changes
Additive and breaking changes as CPN adds fields, values, events, and capabilities, and how to keep your integration compatible.
The Circle Payments Network (CPN) API evolves as Circle adds corridors, payment
methods, blockchains, compliance rules, and failure conditions. Circle
classifies API changes as backward compatible or breaking. Most
backward-compatible changes are additive: they introduce a resource, field,
value, or capability while preserving documented behavior for existing valid
requests. An Originating Financial Institution (OFI) integration must tolerate
these changes so that new response data or event types don't cause client
failures.
## Backward-compatible and breaking changes
Circle expects OFI integrations to accept the changes in the backward-compatible
column without failing or requiring an immediate update. Breaking changes alter
or remove part of the documented contract and may require integration work.
| Area | Backward-compatible change | Breaking change |
| ----------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Endpoints and resources | Add an endpoint or resource | Remove or rename an endpoint or resource |
| Requests | Add an optional parameter when omitting it preserves existing behavior | Add a required parameter, make an optional parameter required, or change the behavior when a parameter is omitted |
| Responses | Add a property | Remove or rename a property, change its data type, or make an always-present property conditionally omitted |
| Enum values | Add a value for a new condition or capability | Remove a value or change what an existing value means |
| Errors | Add an error code for a new failure condition or revise human-readable message text | Change the HTTP status or error code for an existing condition |
| Webhooks | Add an event type or a field to an existing payload | Stop emitting a documented event type, remove a field, or change an existing event's meaning |
| Validation | Relax validation so that a previously rejected request is accepted | Tighten validation so that a previously accepted request is rejected |
Use the [release notes](/release-notes) and relevant migration guides to
identify changes that require integration work. When a migration includes a
deadline, complete the required work in the communicated timeline.
## Build a tolerant integration
Backward-compatible changes don't require your integration to adopt a new
capability. They do require clients to accept response and event data that they
don't use yet.
### Optional request parameters
A new request parameter is backward compatible only when it's optional and
omitting it preserves existing behavior. Existing requests can retain their
current shape until the integration needs the capability controlled by the new
parameter.
### New response fields
Parse the response fields your integration uses and ignore unfamiliar
properties. Strict deserialization that rejects extra properties can turn an
otherwise successful API response into a client-side failure.
### General enum values
Send only documented enum values in requests. For responses, preserve any
unfamiliar raw value and map it to a neutral fallback instead of rejecting the
entire response. Storage and downstream processing must also accept unfamiliar
values.
Don't infer the meaning of an unfamiliar value or use it to trigger an
irreversible action. Add first-class handling after you confirm the value's
documented meaning.
### Lifecycle statuses
An unfamiliar lifecycle status needs a more conservative fallback than a general
enum value. Keep the resource in its current local state, preserve the raw
status, and route the record for review. Don't interpret an unfamiliar status as
success or failure, and don't release funds or take another irreversible action
based on it.
### Resource and reference IDs
The CPN OpenAPI schemas define system-generated resource IDs as UUID-formatted
strings and reference IDs as strings. Treat each identifier according to its
documented schema and as an opaque value: don't derive business meaning from its
shape or impose an undocumented length limit.
### Error codes
Match errors by their machine-readable code, not their human-readable message. A
new code is backward compatible when it represents a new failure condition;
changing the HTTP status or code for an existing condition is breaking. Preserve
and surface an unfamiliar code so that you can add specific handling without
losing the original failure detail.
### Webhook events
After verifying a webhook signature, record or surface an unsupported event type
for follow-up, then acknowledge it with a `2xx` response. This prevents retries
that can't succeed while preserving the event for later handling. Don't process
the event as a known type. Continue to reject webhooks when
[signature verification](/api-reference/verify-webhook-signatures) fails.
# CPN integration concepts
Source: https://developers.circle.com/cpn/concepts/api/api-integration
This page discusses key concepts for integrating with CPN. Understanding these
concepts can help you integrate with CPN more effectively.
## Authentication
CPN authenticates your API requests using a unique key associated with your
account. All authentication is performed in the HTTP header of requests to the
API. If a key is not provided, or an incorrect key is provided, the API returns
a `401 - Invalid Credentials` error. All requests must use HTTPS; the API
rejects any requests using plain HTTP.
Circle provides API keys for authentication with the CPN API. These keys are
provided manually during onboarding. The following is an example of how to set
up the authentication header:
```shell Shell theme={null}
curl --location --request GET 'https://api.circle.com/v1/cpn/payments' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {YOUR_API_KEY}'
```
### Security notes
* Your API key provides full access to the CPN API, so make sure they are stored
securely. They should never be exposed in public repositories and they should
only be shared within your organization using secure methods. As a best
practice, you should rotate your API keys periodically.
* When setting up your API key with Circle, you can request that only certain IP
addresses be allowed to make API calls using the key. Talk to your Circle
representative about establishing an IP allowlist for your API key.
* You should ensure that API requests are always made over TLS/encrypted
connections to prevent the exposure of your key.
* For webhook subscriptions, you must provide an HTTPS endpoint, and validate
the signature with Circle-provided public keys.
## Idempotency
For endpoints that create transactions and payments, CPN requires an idempotency
key to be included in the request body. The idempotency key must be in the UUID
∂v4 format. This allows the API to identify if a repeated request is unique or
duplicate, and prevent unintended duplicate transactions.
To generate an idempotency key, use a UUID generator in your selected
programming language. Generated UUIDs can then be passed to the API as
idempotency keys.
# JSON Schema
Source: https://developers.circle.com/cpn/concepts/api/json-schema
CPN uses [JSON Schema](https://json-schema.org/) to define and validate the
structure of JSON data used to pass RFI data. A JSON Schema is a blueprint for
the compliance data that OFIs must provide. CPN uses the
[Draft 2020-12](https://json-schema.org/draft/2020-12) version of the JSON
Schema standard.
Using JSON Schema to define the structure of this data has the following
benefits:
* **Clarity:** the schema defines every requirement programmatically, including
required fields, data types, formats, and structures. This eliminates
guesswork.
* **Client-side validation:** OFIs can validate their response against the
schema before making the API call, providing instant feedback for developers
and preventing common integration errors.
* **Rich data structures:** schemas support complex requirements like nested
address objects and conditional logic.
## Interpreting JSON Schema
When you call the
[get details for an RFI endpoint](/api-reference/cpn/cpn-platform/get-rfi), the
API returns the JSON Schema for the required response data. The following are
key parts of the schemas to understand when reading them:
* `properties`: defines the fields you can provide in your JSON response
* `required`: contains a list of field names that must be present in the
response
* `$defs` and `$ref`: usable complex objects (defined in `$defs` and referenced
using the `$ref` directive)
## Client-side validation
JSON Schema allows you to validate your response before sending it to the API.
CPN expects that OFIs perform client-side validation against the provided schema
before submitting a response to CPN. Doing so provides a first line of defense
to reduce API errors and failed payments.
Common libraries for JSON Schema validation are:
* **Java:**
[`networknt/json-schema-validator`](https://github.com/networknt/json-schema-validator)
* **Python:**
[`jsonschema`](https://python-jsonschema.readthedocs.io/en/stable/)
* **Node.js:** [`ajv`](https://ajv.js.org/)
### Additional validation
JSON Schema validates structure and format only. You are still responsible for
ensuring the data is contextually correct. The schema ensures that you send the
data in the correct shape; you ensure the data is accurate. For example, you can
[validate CNPJ and CPF numbers using their check digits](/cpn/references/compliance/validating-brazil-tax-account-id)
or [validate IBAN account numbers](/cpn/references/compliance/validating-iban).
# Requests for information
Source: https://developers.circle.com/cpn/concepts/compliance/rfis
When a BFI needs more information about a sender or an
OFI, they create a Request for Information (RFI). This
additional information is required to meet regulatory or risk compliance checks.
The OFI is required to gather the appropriate information and respond to the
request in a set period of time. If the OFI does not respond in time, the
payment associated with the RFI is cancelled.
CPN defines [3 levels of RFI requests](/cpn/references/compliance/rfi-levels),
each with a specific set of fields. The OFI is expected to include all fields
specified by the level when responding to the RFI. Failure to provide the
appropriate fields can lead to payment failure.
The BFI can initiate an RFI at the following points of the payment process:
1. At payment creation
2. After the onchain transaction is complete, but before fiat payout
3. After the payment is complete
Depending on the point in the process where the RFI is created, the OFI may be
notified synchronously in an API response, or asynchronously by webhook. RFIs
are typically resolved by returning the relevant information through the API.
RFIs can also be raised and resolved through direct support tickets with CPN.
# CPN component states and workflows
Source: https://developers.circle.com/cpn/concepts/payments/component-states-and-workflows
This page contains information about the states that high-level components of
the CPN system can be in. Your integration should be able to handle and respond
to each of the various states that a given component may present. In some cases,
the state of a component requires immediate or rapid action from an OFI.
Reference these states as a way to confirm your integration is comprehensive.
## Payments
A payment represents the end-to-end CPN payment flow, including the onchain
transaction and RFI check. It is initiated by locking in a quote and providing
the required recipient details.
Payments have the following workflow:
### Payment States
| State | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CREATED` | A quote has been accepted and the payment is initialized.
When a payment requires an RFI in the created state the RFI must be resolved before the payment can proceed.
A payment can be in the created state without an associated RFI while the BFI completes its compliance check. This can take up to 1 business day to complete. |
| `CRYPTO_FUNDS_PENDING` | The payment has passed a compliance check and is waiting for the onchain transaction of crypto.
The OFI is required to complete the onchain transaction in order to proceed. |
| `FIAT_PAYMENT_INITIATED` | The BFI has received and validated the crypto transfer and has initiated the fiat payment. |
| `COMPLETED` | The BFI has completed the fiat transfer. Depending on the transfer method, the receiver may or may not have received the transfer. |
| `FAILED` | The payment can't be completed. |
A payment can also include an optional `statusAddendum` field that provides
additional context about the current payment status. This field does not
represent a separate state—it supplements the main `status` field. CPN omits
`statusAddendum` from the response when no additional context applies.
| Status addendum | Associated status | Description |
| ---------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `IN_MANUAL_REVIEW` | `CREATED` | The payment is on hold for a compliance or manual review. The payment remains in the `CREATED` state until the review is complete. |
| `PAYMENT_SETTLEMENT_DELAYED` | `FIAT_PAYMENT_INITIATED` | Settlement timing has slipped. The payment remains in progress while the delay is resolved. |
When a payment is initiated, the BFI performs an initial payment review, which
affects the state that the payment is created in:
* If the BFI approves the initial payment review outright, the payment is
created in the `CRYPTO_FUNDS_PENDING` state.
* If the BFI rejects the initial payment review outright, the payment is created
in the `FAILED` state.
* If the BFI needs to take additional steps to review the payment, the payment
is created in the `CREATED` state. Final approval or rejection of the payment
is sent asynchronously.
## RFIs
Requests for information (RFI) is a process that is initiated by the BFI when
more detailed information about the sender is required to meet regulatory or
risk compliance checks.
RFIs have the following workflow:
### RFI states
| State | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `INFORMATION_REQUIRED` | Initial status. OFI is required to submit the requested data to resolve the RFI. |
| `IN_REVIEW` | OFI has submitted the requested data and BFI is reviewing. |
| `APPROVED` | BFI approves the additional information. |
| `FAILED` | BFI rejects the additional information. This is a resolved state where the OFI can't resubmit additional information to the BFI. |
## Refunds
Refunds occur when the BFI can't complete the transfer of fiat to the receiver.
The BFI creates an onchain transaction to transfer the cryptocurrency back to
the OFI.
Refunds have the following workflow:
### Refund states
| State | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CREATED` | The refund is initialized but the onchain transaction has not been created yet. Some BFIs may skip this status and create the refund directly into the `COMPLETED` or `FAILED` state. |
| `FAILED` | The onchain transaction failed. |
| `COMPLETED` | The onchain transaction was successfully broadcast and confirmed by the BFI. The transaction hash is available. |
## Transactions
A transaction on CPN is a data object that represents a funds transfer
transaction on the blockchain. When the payment is in the `CRYPTO_FUNDS_PENDING`
state, the OFI should initiate the onchain transaction with CPN as soon as
possible.
Transactions have the following workflow:
### Transaction states
| State | Description |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `CREATED` | Transaction has been created and the payload returned to the caller, awaiting signature. |
| `PENDING` | OFI has submitted the signed transaction. The transaction is pending broadcast to the blockchain. |
| `BROADCASTED` | The signed transaction has been broadcast to the blockchain. |
| `COMPLETED` | The transaction is confirmed onchain. |
| `FAILED` | The transaction can't be completed. |
## Support tickets
The CPN support tickets API allows you to raise and manage transaction-related
issues such as settlement delays, missing information, or refunds. These tickets
are stored in the CPN platform and routed to the appropriate party for
resolution.
# Payment reference
Source: https://developers.circle.com/cpn/concepts/payments/payment-reference
When you
[create a payment with the CPN API](/api-reference/cpn/cpn-platform/create-payment),
you should provide the `refCode` field to carry a meaningful reference for the
transaction. This field can be either an invoice ID or a customer reference. How
this reference appears on the beneficiary's bank statement depends on the
capabilities of the payment rail, which is handled as follows:
* **Metadata support by rail and BFI:** The sender's name (OFI's customer name)
from the travel rule data appears directly on the recipient's bank statement
as the ultimate remitter, and the `refCode` appears in the memo field. This
provides the highest level of transparency for the recipient to recognize and
reconcile the payments.
* **Partial metadata support:** If the payment rail doesn't support displaying
the sender's name separately but allows a memo/reference field, the BFI
populates this memo with a concatenated string in the format
`{refCode} + {Sender Name}`
* **Minimal metadata support:** If neither the sender name or reference field is
supported on the rail, Circle returns a `fiatNetworkPaymentRef`, which is a
rail-specific transaction reference. This ID is visible to the beneficiary on
their bank statement, and is returned to the OFI by webhook and in the
[get payment endpoint](/api-reference/cpn/cpn-platform/get-payment).
# Payment
Source: https://developers.circle.com/cpn/concepts/payments/payments
A payment represents the end-to-end CPN payment flow, including the onchain
transaction and RFI check. It is initiated by locking in a quote and providing
the required recipient details.
The payment process begins when an OFI locks in a quote and submits sender and
beneficiary details. The transaction undergoes compliance checks, including the
RFI process if required. Once all necessary compliance checks are completed, the
payment enters the onchain transaction phase, where it awaits the transfer of
USDC. After the crypto transfer is received, the BFI initiates a fiat transfer
to the final recipient.
During the fiat transfer, the CPN sends webhook notifications to the OFI
indicating that the transfer has been confirmed or canceled. If the fiat
transfer is canceled or fails for any reason, the BFI issues a refund of crypto
to the OFI.
# Quotes
Source: https://developers.circle.com/cpn/concepts/quotes
Quotes are real-time exchange rates and settlement costs for a specific payment
corridor. A quote includes the following key elements:
* **Exchange rate lock**: The quote locks in an exchange rate for a specified
time window, protecting both the OFI and the sender from market volatility
during the payment process. When the time window expires, a new quote must be
requested.
* **Two-way quotes**: You may query the quote based on the source amount and the
destination amount. For instance, if a customer holding USDC wants to pay a
recipient in BRL and the recipient needs an accurate amount of BRL, you can
query using the destination amount. If a quote is for a remittance use case,
where the customer needs an accurate amount of USDC to send, you can query
using the source amount.
* **Cost breakdown**: The quote response includes details of all applicable
fees, including fee and any additional charges at the transaction level,
ensuring transparent cost estimation.
* **Competitive aggregation**: By providing multiple quotes, CPN ensures that
OFIs can choose the most competitive option based on price, speed, and
compliance.
## Payment use case
A quote also determines the use case of the payment you create from it. The use
case isn't a field you set. CPN derives it from the `senderType` and
`recipientType` you request the quote with.
| `senderType` | `recipientType` | `useCase` |
| ------------ | --------------- | --------- |
| `BUSINESS` | `BUSINESS` | `B2B` |
| `BUSINESS` | `INDIVIDUAL` | `B2C` |
| `INDIVIDUAL` | `BUSINESS` | `C2B` |
| `INDIVIDUAL` | `INDIVIDUAL` | `C2C` |
When you [create a payment](/api-reference/cpn/cpn-platform/create-payment), the
`useCase` you send must match the combination of `senderType` and
`recipientType` you requested the quote with. Any other value is rejected with
error code [`290211`](/api-reference/cpn/error-codes). Because the quote
response doesn't include the use case, keep a record of both types alongside the
quote ID.
# Block confirmation and transaction finality
Source: https://developers.circle.com/cpn/concepts/transactions/block-confirmation-and-finality
When you submit a transaction to CPN, it is initially in the `pending` state and
waiting to be broadcast to the blockchain. Once the transaction is broadcast to
the blockchain, it transitions to the `broadcasted` state, and the transaction
is waiting to be included in a block.
CPN monitors each block of supported blockchains and the status of CPN
transactions on those chains, updating the status as necessary.
## Block confirmation
Block confirmation is the process of validating and adding transactions to a
block on a blockchain. Each time a new block is added to a blockchain, it
confirms the previous blocks.
Without a sufficient number of confirmations, transactions are vulnerable to
alteration through blockchain reorganization. Blockchain reorganization occurs
when validators discard one or more blocks that were previously part of the
canonical chain and replace them with a different set of blocks, rewriting part
of the chain.
**Note:** Blockchain reorganizations can happen for a variety of reasons. It's
an engineering best practice to expect reorganizations and make your system
resilient to them.
Each additional block confirmation added after a block makes that block less
likely to be removed in a reorganization. Each block confirmation adds
confidence that the transaction is permanently included in the blockchain.
## Confirmation number
For a given block, the confirmation number is the number of subsequent blocks
added to the blockchain.
For a given transaction included in a block, CPN waits for a set number of
blocks (in addition to the original) to be added to the chain before it
considers the transaction final. This number varies across supported
blockchains. When a sufficient number of blocks are appended after the original
block, CPN updates the status of the transaction to `completed` and notifies
both the OFI and BFI of the status change.
**Note:** Even if a transaction is visible on a block explorer, it doesn't
necessarily mean that the transaction is considered `completed` by CPN,
because it's possible that it hasn't reached the number of block confirmations
for finality.
## Reorganization risk
Although Circle may broadcast transactions initiated through CPN to the
blockchain network, Circle cannot guarantee that the transaction is recorded or
permanently confirmed by the network. Transactions over CPN may be dependent on
underlying blockchain networks that Circle does not control, and CPN
Participants assume all risks associated with the blockchain network operations
and any operating changes, including in the unlikely event of a deep
reorganization or other invalidation of previously confirmed transactions.
Circle shall not be liable for any loss or damage arising from issues with, or
any delays, reversals, or failures of transactions caused by such operations and
operating changes.
# Transaction
Source: https://developers.circle.com/cpn/concepts/transactions/transaction-overview
A transaction in the CPN API represents a transfer of funds onchain to fund the
payment. Once payment is in the `CRYPTO_FUNDS_PENDING` status, the OFI should
create the onchain transaction using CPN's transaction endpoints. Through these
endpoints, the OFI can initiate and broadcast the transaction. The OFI uses its
own wallet integration to sign transactions before sending them to the API. CPN
provides transaction updates through webhooks. Transactions follow these steps:
1. The OFI initiates a transaction request, and Circle returns the raw
transaction data
2. The OFI validates the response and signs the transaction
3. The OFI submits the signed transaction to the CPN API
4. CPN verifies the transaction content and broadcasts it to the blockchain
5. CPN notifies the OFI and the BFI of the transaction status
# Transactions V2
Source: https://developers.circle.com/cpn/concepts/transactions/transactions-v2
Transactions V2 is CPN's onchain transaction path. It enables secure and
compliant stablecoin settlement across all blockchains that
[CPN supports](/cpn/references/blockchains/supported-blockchains), managing gas
fees, signing, and orchestration on your behalf.
## Gas abstraction
Transactions V2 provides gas abstraction which removes the requirement for you
to pay native tokens for gas fees during transaction broadcast. This provides a
lower-complexity operational model for you by eliminating the need to acquire
and maintain a native token balance in your wallet.
When a Transaction V2 quote is created, the `fees` field includes a fixed gas
fee denoted in USDC. This gas fee is valid as long as the payment remains
active. During transaction settlement, the payment settlement smart contract
withdraws the fee amount from your wallet and distributes it to the beneficiary.
The transaction fee you pay is fixed regardless of fluctuations of the native
blockchain gas fee levels. CPN ensures that the transaction gets broadcast
accurately and on time. This removes the need for manual acceleration and
monitoring by delegating it to CPN.
## EVM payment settlement contract and `Permit2`
In EVM blockchains, Transactions V2 uses a payment settlement smart contract
that allows you to send verified payments to the BFI that are authorized by you,
an attester (CPN), and (optionally) the BFI. The contract ensures that the
transaction is accurate and correct, and can serve as proof of payment after the
transaction has settled.
The payment settlement contract uses the `Permit2` contract, Uniswap's universal
token approval system, for token approvals. `Permit2` makes integrations more
straightforward by using gasless, offchain signatures for token transfers.
`Permit2` permits are signature-based, time-bound, and single-use.
The payment settlement smart contract uses `Permit2` to execute funds transfer
from your wallet to the recipient wallet in a single contract execution. In
practice, this means that you sign an EIP-712 typed-data message of the
`PermitWitnessTransferFrom` method call to allow the payment settlement smart
contract to transfer USDC out of your wallet and into the recipient wallet.
```json JSON theme={null}
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"PermitWitnessTransferFrom": [
{ "name": "permitted", "type": "TokenPermissions" },
{ "name": "spender", "type": "address" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" },
{ "name": "witness", "type": "PaymentIntent" }
],
"TokenPermissions": [
{ "name": "token", "type": "address" },
{ "name": "amount", "type": "uint256" }
],
"PaymentIntent": [
{ "name": "from", "type": "address" },
{ "name": "to", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "validAfter", "type": "uint256" },
{ "name": "validBefore", "type": "uint256" },
{ "name": "nonce", "type": "bytes32" },
{ "name": "beneficiary", "type": "address" },
{ "name": "maxFee", "type": "uint256" },
{ "name": "requirePayeeSign", "type": "bool" },
{ "name": "attester", "type": "address" }
]
},
"domain": {
"name": "Permit2",
"chainId": 11155111,
"verifyingContract": "PERMIT2_ADDRESS"
},
"message": {
"permitted": {
"token": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"amount": "100050000"
},
"spender": "CPN_SMART_CONTRACT_ADDRESS",
"nonce": "0",
"deadline": "1234567890",
"witness": {
"from": "OFI_PAYER_ADDRESS",
"to": "BFI_PAYEE_ADDRESS",
"value": "100000000",
"validAfter": "0",
"validBefore": "1234567890",
"nonce": "ONCHAIN_PAYMENT_REF",
"beneficiary": "CIRCLE_BENEFICIARY_ADDRESS",
"maxFee": "50000",
"requirePayeeSign": false,
"attester": "CPN_WALLET_ADDRESS"
}
},
"primaryType": "PermitWitnessTransferFrom"
}
```
### `Permit2` allowance
The use of `Permit2` requires you to grant an allowance of USDC to the `Permit2`
contract ahead of the payment. This allowance grants the `Permit2` contract
permission to transfer USDC from your wallet in a CPN settlement. When you sign
a CPN transaction, that transaction allows the payment settlement contract to
consume the USDC allowance previously granted to `Permit2`.
The allowance amount can be set to a specific value based on the expected
payment volume or to the maximum `uint256` value for unlimited transfers. The
allowance is the foundational authorization that makes the entire CPN payment
settlement system possible through `Permit2`'s signature-based transfer
mechanism.
# Wallet nonce management
Source: https://developers.circle.com/cpn/concepts/transactions/wallet-nonce-management
**Note:** This topic applies to EVM blockchains only. For [Transactions
V2](/cpn/concepts/transactions/transactions-v2), nonces are managed by CPN so
you don't need to actively manage them.
This topic explains how nonces work in Ethereum Virtual Machine (EVM)
transactions. It also gives you tips for managing nonces when you handle many
transactions at once.
Before you read this, you should know the basics about EVM wallets and how
transactions are signed.
## What is a nonce?
In EVM-based blockchains, a wallet nonce is a counter. It tracks how many
transactions you sent from a specific wallet address. Each transaction must use
a different nonce. You must use nonces in order, one after another.
Key points:
* New wallets start with nonce 0
* Each successful transaction adds 1 to the nonce
* You must use nonces in order (no gaps)
* Each wallet address has its own nonce count
## Nonce conflicts in concurrent transactions
Problems can happen when you create many transactions at the same time from one
wallet address.
This problem often happens when many users create transactions at once, and your
system uses the same EVM wallet for all these requests.
When signing transactions, multiple threads in your system might try to get the
wallet's current nonce at the same time. If this race condition occurs, multiple
transactions might get assigned the same nonce number. When this occurs, only
the first transaction works. The others fail.
Another common problem happens when transactions get stuck in the blockchain's
mempool and block later transactions. Here's an example:
* Your CPN integration signs two transactions from the same wallet in the right
order:
* Transaction A with nonce 5
* Transaction B with nonce 6
* Transaction A gets rejected by CPN because it has the wrong parameters
* Transaction B goes to the blockchain but gets stuck in the mempool because the
blockchain needs nonce 5 first
* All future transactions from this wallet get blocked until you fix the gap
## Best practices for managing nonces
Your integration may vary based on your systems. Here are some general tips for
managing nonces:
**Set up one central system to track nonces for each wallet.** This system
should safely handle nonce assignment when multiple threads request to sign
transactions at once. Make sure each nonce gets assigned only once to prevent
duplicates. You can use a locking system or a queue system to manage nonce
assignment when handling multiple operations.
**Keep your system in sync with the blockchain to track the latest nonce
numbers.** Your local nonce tracking should reset its counter if it gets out of
sync with the blockchain's nonce state. Set up regular health checks to find
nonce gaps.
You should sync with the blockchain:
* Before starting new transaction batches
* After transactions fail or get rejected
* When your systems restart after downtime
# Bring your own wallet for CPN
Source: https://developers.circle.com/cpn/concepts/wallets/bring-your-own-wallet
How using your own wallet infrastructure fits into Circle Payments Network integrations.
Circle Payments Network (CPN) requires a blockchain wallet to sign and submit
USDC transfer transactions on behalf of your organization. Circle doesn't
require this wallet to be a [Circle Wallet](/wallets/dev-controlled.mdx).
Institutions that already custody USDC and operate keys through an in-house or
third-party wallet stack can use that existing infrastructure with CPN.
Originating Financial Institutions (OFIs) that hold their own USDC often choose
this Bring Your Own Wallet (BYOW) approach and it's available on all
[blockchains that CPN supports](/cpn/references/blockchains/supported-blockchains).
## When to bring your own wallet
Bring Your Own Wallet (BYOW) is a good fit when your organization:
* Already operates wallet infrastructure with established key-management
procedures.
* Requires custody arrangements that are specific to your compliance or
regulatory environment.
* Manages USDC balances and blockchain interactions through an existing
third-party provider.
If none of these apply, you can have Circle host the operational wallet instead.
See how to
[Set up a Circle wallet for CPN payments](/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments).
## Your responsibilities
When you bring your own wallet, you take on operational responsibilities that
Circle otherwise handles. The following table summarizes how those
responsibilities are divided.
| Responsibility | BYOW | Circle-hosted wallet |
| ----------------------------------------- | ---- | -------------------- |
| Key management and signing | You | Circle |
| Blockchain support and connectivity | You | Circle |
| Nonce handling | You | Circle |
| USDC balance management | You | You |
| Native gas token funding (where required) | You | Circle |
CPN Transactions V2 provides unsigned transaction data that includes nonce
values, so you don't need to manage nonces directly. See the [Create
Transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint for details.
Your wallet implementation must satisfy the technical expectations described in
[Wallet provider compatibility](/cpn/references/blockchains/wallet-provider-compatibility).
## How CPN works with your wallet
Regardless of which wallet option you choose, you interact with the same CPN
APIs. Payment creation, settlement, and reporting work identically; only the
custody and signing layer differs. Transactions V2 returns a complete unsigned
payload, including nonce values.
The interaction between CPN and your wallet follows this pattern:
1. You call the CPN
[Create Transaction](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint to prepare unsigned transaction data for a payment.
2. CPN returns unsigned transaction data (the payload your wallet needs to
sign).
3. Your wallet infrastructure signs the transaction using its own key-management
system.
4. You submit the signed transaction back to CPN through the
[Submit Transaction](/api-reference/cpn/cpn-platform/submit-transaction-v2)
endpoint.
5. CPN validates the signed transaction and broadcasts it to the blockchain.
6. CPN sends [webhook notifications](/api-reference/webhooks) with transaction
status updates.
CPN never accesses your private keys. It issues payment instructions that your
wallet infrastructure executes by signing and submitting transactions to the
appropriate blockchain.
The same CPN transaction APIs apply whether you sign with Circle or with your
own keys, so migrating between a Circle-hosted wallet and BYOW does not require
changes to your CPN integration logic. For a full walkthrough, see the
[Integrate with CPN as an OFI](/cpn/quickstarts/integrate-with-cpn-ofi)
quickstart.
# Webhook retries
Source: https://developers.circle.com/cpn/concepts/webhooks/webhook-retries
When a webhook can't be delivered successfully (2xx status code returned), CPN
continues to try to deliver the webhook. To avoid encountering the same
transient error too many times, the retries are sent with a delay between them.
The number of retries varies between testing and production:
* **Testing:** 6 retries
* **Production:** 11 retries
The following table outlines the schedule for webhook retries:
| Retry attempt | Delay |
| ------------- | ---------- |
| 1 | 1 second |
| 2 | 10 seconds |
| 3 | 30 seconds |
| 4 | 1 minute |
| 5 | 15 minutes |
| 6 | 1 hour |
| 7 | 3 hours |
| 8 | 5 hours |
| 9 | 6 hours |
| 10 | 10 hours |
| 11 | 11 hours |
# Fiat payouts
Source: https://developers.circle.com/cpn/fiat-payouts
Send cross-border fiat payouts with real-time FX quoting, USDC settlement, and built-in compliance.
Fiat Payouts is the cross-border fiat payout product in
[Circle Payments Network (CPN)](/cpn). Using onchain primitives and built-in
compliance capabilities, it delivers near-instant cross-border settlement: an
Originating Financial Institution (OFI) sends USDC through CPN, and a
Beneficiary Financial Institution (BFI) delivers local fiat to the receiver.
Participating institutions can expand their global footprint with a single
integration.
This developer documentation focuses on **Originating Financial Institutions
(OFIs)** that facilitate cross-border payments for their customers. The main
institution roles in CPN are:
Work with senders, onramp fiat to crypto, and connect through CPN for
cross-border payments.
Receive stablecoins through CPN and offramp into local fiat for receivers in
the destination market.
Coordinates OFIs and BFIs by aggregating quotes, routing payments, and
managing onchain settlement between participants.
CPN is part of Circle's broader ecosystem of financial products and is designed
to work with the USDC ecosystem for global payment operations.
## Prerequisites
As an OFI, plan for the following before onboarding:
* Access to USDC liquidity from your own sources or through Circle. If you use
Circle, see
[How-to: Set up Circle on/off-ramps for CPN payments](/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments)
for fiat-to-USDC and related transfers.
* A custodial or signing solution for USDC and technical capability to interact
with blockchains (sign and observe transactions). If you use Circle for the
operational wallet, see
[How-to: Set up a Circle wallet for CPN payments](/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments).
If you use your own wallet stack, see
[Bring your own wallet for CPN](/cpn/concepts/wallets/bring-your-own-wallet)
and the
[Wallet provider compatibility](/cpn/references/blockchains/wallet-provider-compatibility)
reference.
* Established Know Your Customer (KYC) and Anti-Money Laundering (AML) processes
in place for your customers
Circle Wallets and Circle On/Off-Ramps are **optional** capabilities for CPN.
Many institutions already hold USDC and use their own wallets. The same
developer documentation applies to every CPN customer; the linked how-tos are
for teams that choose Circle for those layers. If you're a BFI or payout
partner, Circle may reach out proactively as CPN grows.
## Key features
CPN returns quotes from several BFIs in one step and locks the FX rate for
the payment, limiting exchange-rate risk for the OFI and customer.
Each payment is assigned to a BFI according to the OFI's pricing and
operational preferences, for example cost and settlement speed.
Payments are settled using USDC across [supported
blockchains](/cpn/references/blockchains/supported-blockchains) and
exchanged to local fiat currencies for the receiver through the BFI.
REST APIs and webhooks support payment flows and status monitoring, with
detailed documentation and support for integrators.
Travel rule and beneficiary account data is encrypted by the OFI throughout
the payment process and can only be decrypted by the designated BFI.
CPN allows institutions to create and manage support tickets for payments,
disputes, and reversals through the API. Circle handles routing and
resolution across the network.
**CPN White Paper**
Read the
[CPN White Paper](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/Whitepapers/CPN_Whitepaper.pdf)
to learn more about the motivation for creating CPN and its intended use cases.
## Entities in CPN
The following entities are referenced in this documentation and make up the key
entities that allow CPN to perform cross-border transactions.
* **Senders** (external): Businesses or individuals initiating cross-border
payments
* **Receivers** (external): Businesses or individuals receiving cross-border
payments
* **Originating Financial Institutions (OFIs)**: Financial institutions that
interface with senders, onramp fiat to USDC, and connect with BFIs through CPN
for transfer and fiat offramp
* **Beneficiary Financial Institutions (BFIs)**: Financial institutions that
receive USDC from OFIs through CPN and provide an offramp to local fiat
currency
* **Circle Payments Network (CPN)**: Orchestration layer that aggregates
financial institutions, vets participants, and facilitates money movement
through onchain and offchain services.
## CPN workflow
The following is a generic example of how a payment in CPN is performed. In this
example, the OFI has a sender in the United States. The sender already has an
account and completed KYC with the OFI. The sender wants to pay a receiver in
Brazil. The sender wants to pay with USD and the receiver wants to receive the
payment in Brazilian real (BRL).
* OFI requests a list of quotes for the payment (USDC to BRL) from CPN
* CPN aggregates quotes from multiple BFIs
* CPN returns a list of quotes to the OFI
* OFI selects the best quote or asks the sender to select their preferred
quote
* Sender accepts the quote
* OFI gets the requirements for travel rule data and beneficiary account
information
* OFI creates a payment request (USDC to BRL) in CPN
* OFI collects and
[encrypts](/cpn/guides/payments/encrypt-travel-rule-beneficiary-data)
necessary payment details, such as travel rule data and beneficiary account
information, and includes the encrypted data in the payment request
* BFI reviews the travel rule data (and may [request additional
information](/cpn/concepts/compliance/rfis)) and approves the payment
request
* OFI ensures the required USDC balance is available in the operational
wallet that will sign the transfer (onramping or internal treasury
movements may be required first)
* OFI requests an onchain transaction object to transfer USDC to the
destination address from CPN to sign
* OFI signs the onchain transaction object and sends it to CPN
* CPN validates and broadcasts the transaction
* CPN notifies the OFI and BFI when the onchain transaction is confirmed
* BFI initiates fiat payment after the onchain transaction is confirmed
(transfer BRL to receiver's bank account)
* BFI notifies CPN when the fiat payment is initiated and complete
* CPN notifies the OFI with the appropriate payment statuses
* OFI can query payment status at any time from the CPN API
* CPN provides real-time status updates on the payment via webhooks
* You can use payment history and reporting for reconciliation
## Get started
Run the OFI quickstart, learn how the CPN API fits together, then subscribe to
payment webhooks:
Request a quote, create a payment, complete the onchain USDC transfer, and
track status.
Understand how CPN APIs work together, including quotes, payments,
transactions, and the JSON request and response model.
Expose a subscriber endpoint and register for CPN notifications so your
systems react to payment and transaction updates.
# How-to: Set up Circle on/off-ramps for CPN payments
Source: https://developers.circle.com/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments
Use Circle APIs to move fiat and USDC in support of CPN payment flows (onramp liquidity and optional offramp).
This guide shows how to use Circle Mint account APIs for CPN payment flows when
using Circle for treasury and settlement. Use it to onramp fiat into USDC and
fund your operational wallet, and to offramp USDC to fiat when you need to.
## Overview
* Use this guide when you want Circle to supply USDC liquidity (fiat in, USDC to
your operational wallet) or to offramp USDC to fiat.
* CPN does not require Circle on/off-ramps. You can source USDC from any
compliant channel that meets your compliance and settlement needs.
* Circle on/off-ramp behavior is provided by Circle APIs (Circle Mint and
related account APIs). Start with
[Getting started with the Circle APIs](/circle-mint/quickstarts/getting-started)
for authentication and environments.
Each step links to the Circle Mint API reference page for that operation,
where request and response fields are fully defined. The steps map those
Circle APIs to CPN scenarios.
## Prerequisites
Before you begin, ensure you have:
* A [CPN Console](https://cpn.circle.com/signin) account with the Circle
On/Off-Ramps capability enabled for your organization (after your CPN
eligibility application is approved).
* An API key with Circle On/Off-Ramps (Circle APIs) permissions. Create and
manage keys in
[CPN Console → Developer → API Keys](https://cpn.circle.com/signin).
* Sandbox or mainnet access enabled for Circle On/Off-Ramps.
* An operational wallet address for USDC. To use a Circle wallet, see
[Set up a Circle Wallet for CPN payments](/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments);
otherwise use your own wallet infrastructure.
Mainnet access for Circle On/Off-Ramps is typically granted after your CPN
eligibility application is approved and you request that capability. Sandbox
uses the same request and response structure as production.
This guide uses **Circle Mint** account APIs. For Mint, use the sandbox and
production [API hosts](/circle-mint/references/sandbox-and-testing) and the API
key for each environment. **CPN Platform** payment APIs use one base URL: the
API key determines whether requests run in sandbox or production.
## Onramp: Deposit fiat and transfer USDC to your operational wallet
As an Originating Financial Institution (OFI), you convert fiat to USDC and move
USDC to the wallet that signs CPN onchain transfers.
### Step 1: Link your bank account
Register the bank account you use to send fiat deposits. Use
[Create wire bank account](/api-reference/circle-mint/account/create-business-wire-account)
(`POST /v1/businessAccount/banks/wires`). For a guided walkthrough, see
[Deposit Fiat](/circle-mint/howtos/deposit-fiat).
### Step 2: Retrieve wire instructions and send funds
Retrieve instructions with
[Get wire transfer instructions](/api-reference/circle-mint/account/get-business-wire-account-instructions)
(`GET /v1/businessAccount/banks/wires/{id}/instructions`). Send the wire from
your bank. Include the tracking reference (or equivalent memo) Circle provides
so the deposit can be matched to your account.
In sandbox, simulate a deposit with
[Create mock wire payment](/api-reference/circle-mint/account/create-mock-wire-payment)
(`POST /v1/mocks/payments/wire`). Mock wires can take up to 15 minutes to
process.
### Step 3: Confirm your Circle business account balance
After the deposit is credited, confirm USDC with
[List balances](/api-reference/circle-mint/account/list-business-balances)
(`GET /v1/businessAccount/balances`).
### Step 4: Configure notifications for deposit status (optional)
Optionally subscribe to status notifications so your backend learns when a
deposit completes. Use
[Create subscription](/api-reference/circle-mint/general/create-subscription)
(`POST /v1/notifications/subscriptions`). The request body requires a public
HTTPS endpoint that can complete the
[AWS SNS subscription handshake](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html)
(see
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications)):
```json theme={null}
{
"endpoint": "https://your-endpoint.example.com/webhooks"
}
```
Notification payloads use a `notificationType` field. For flows in this guide,
common values include `transfers` (onchain transfers) and `payouts` (bank
payouts); see
[Notifications data models](/circle-mint/references/webhook-notifications) for
payload shapes.
You can also manage subscriber endpoints in
[CPN Console → Developer → Webhooks](https://cpn.circle.com/signin) when your
organization exposes that UI for your Circle APIs integration.
### Step 5: Transfer USDC to your operational wallet
Allowlist your operational wallet address with
[Create recipient address](/api-reference/circle-mint/account/create-business-recipient-address)
(`POST /v1/businessAccount/wallets/addresses/recipient`), then send USDC onchain
with
[Create transfer](/api-reference/circle-mint/account/create-business-transfer)
(`POST /v1/businessAccount/transfers`). Track completion with
[Get transfer](/api-reference/circle-mint/account/get-business-transfer)
(`GET /v1/businessAccount/transfers/{id}`).
Example body for **create recipient** (see the API reference for all fields and
valid `chain` values; `description` is required):
```json theme={null}
{
"idempotencyKey": "",
"address": "",
"chain": "ETH",
"currency": "USD",
"description": "CPN operational wallet"
}
```
Example body for **create transfer** with destination type `blockchain`:
```json theme={null}
{
"idempotencyKey": "",
"source": { "type": "wallet", "id": "merchant" },
"destination": {
"type": "blockchain",
"address": "",
"chain": "ETH"
},
"amount": { "amount": "", "currency": "USD" }
}
```
When the transfer completes, your operational wallet is ready for CPN onchain
steps.
A successful create-transfer response returns a `status` of `pending` and a
transfer `id`. Poll
[Get transfer](/api-reference/circle-mint/account/get-business-transfer)
(`GET /v1/businessAccount/transfers/{id}`) until the status reaches `complete`.
If the status is `failed`, check the `errorCode` field: common values include
`transfer_failed`, `transfer_denied`, and `insufficient_funds`. Do not reuse the
same `idempotencyKey` for a new request with different parameters; retrying with
identical parameters returns the original response. See
[API errors](/api-reference/circle-mint/error-codes#transfer-error-codes) and
[Idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests).
## Offramp: Receive USDC and pay out to your bank account
As an OFI, you may need to offramp USDC to fiat: for example, when converting
received USDC to local currency, handling payment failures, or processing
returns. The steps below reverse the onramp flow: deposit USDC into your Circle
business account and initiate a bank payout.
### Step 1: Link your bank account
Use
[Create wire bank account](/api-reference/circle-mint/account/create-business-wire-account)
(`POST /v1/businessAccount/banks/wires`) for the account that will receive fiat
payouts.
### Step 2: Create a deposit address for your Circle business account
Generate an onchain deposit address with
[Create deposit address](/api-reference/circle-mint/account/create-business-deposit-address)
(`POST /v1/businessAccount/wallets/addresses/deposit`). USDC sent to that
address credits your Circle business account balance.
Example body (see the API reference for required fields and valid `chain`
values):
```json theme={null}
{
"idempotencyKey": "",
"currency": "USD",
"chain": "ETH"
}
```
### Step 3: Configure notifications for inbound activity
Subscribe with
[Create subscription](/api-reference/circle-mint/general/create-subscription)
(`POST /v1/notifications/subscriptions`) using an HTTPS endpoint as in the
onramp step. Your handler receives payloads whose `notificationType` values are
documented in
[Notifications data models](/circle-mint/references/webhook-notifications). For
example, `transfers` notifications reflect onchain transfer status changes.
Where available, you can configure endpoints in
[CPN Console → Developer → Webhooks](https://cpn.circle.com/signin), in addition
to using the API.
CPN payment lifecycle events still use CPN subscriptions (see
[Webhook events](/cpn/references/webhooks/webhook-events)).
### Step 4: Offramp USDC to your bank account
Verify your balance with
[List balances](/api-reference/circle-mint/account/list-business-balances)
(`GET /v1/businessAccount/balances`), then create a payout with
[Create payout](/api-reference/circle-mint/account/create-business-payout)
(`POST /v1/businessAccount/payouts`).
Example body (see the API reference for all required fields):
```json theme={null}
{
"idempotencyKey": "",
"destination": {
"type": "wire",
"id": ""
},
"amount": { "amount": "", "currency": "USD" }
}
```
A successful response returns a `status` of `pending` and a payout `id`. Track
completion with
[Get payout](/api-reference/circle-mint/account/get-business-payout)
(`GET /v1/businessAccount/payouts/{id}`). The payout moves through `pending` to
`complete` or `failed`. If the status is `failed`, check the `errorCode`
field—common values include `insufficient_funds`, `transaction_denied`, and
`bank_transaction_error`. See
[API errors](/api-reference/circle-mint/error-codes#payout-error-codes) and
[Idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests).
## See also
* With your operational wallet funded, return to
[Integrate with CPN as an OFI](/cpn/quickstarts/integrate-with-cpn-ofi) at
Part 1: Request a quote.
* [Set up a Circle Wallet for CPN payments](/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments)
for operational wallet setup with Circle dev-controlled wallets.
# How-to: Respond to an RFI
Source: https://developers.circle.com/cpn/guides/compliance/respond-to-rfi
This guide explains how to respond to an RFI request that is system-initiated.
In a system-initiated RFI, you are notified either synchronously when a payment
is created, or asynchronously by an RFI webhook.
This guide doesn't cover how to respond to an RFI when it is initiated by a
support ticket. For RFIs initiated this way, you must follow the support ticket
workflow.
## Steps
When a system-initiated RFI is created by a BFI, you are notified in one of two
ways:
1. In the synchronous response to the payment creation endpoint. When you create
a payment, the endpoint responds with the created payment record containing a
non-null `activeRfi` field.
2. Asynchronously by an RFI webhook. This can occur at any time in the payment
flow after the payment is created. The RFI webhook doesn't contain the entire
RFI object.
Once you have the full RFI object, you can use the following steps to respond to
the RFI.
### Step 1: Get the RFI requirements and certificate
Before you respond to an RFI, you need to get the requirements and the
certificate used for encrypting RFI responses.
The RFI requirements are available in the RFI object. If the RFI was created
synchronously, the full object is included in the `activeRfi` field of the
payment object. If the RFI was created asynchronously, you must retrieve the
full object using the
[get details for an RFI endpoint](/api-reference/cpn/cpn-platform/get-rfi). The
following is an example RFI object:
```json JSON theme={null}
{
"id": "48dfd36c-cf6c-4d9d-b065-69bdbe9bfea1",
"paymentId": "90dfd86c-cf6c-4d9d-b065-69bdbe9bfec8",
"status": "INFORMATION_REQUIRED",
"level": "LEVEL_1",
"expireDate": "2025-01-28T19:41:33.076Z",
"fieldRequirements": {
"version": 1,
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RFI Requirements",
"type": "object",
"description": "RFI Requirements",
"properties": {
"DATE_OF_BIRTH": {
"description": "Individual's date of birth (YYYY-MM-DD)",
"type": "string"
},
"NAME": {
"description": "Individual's full name",
"type": "string"
}
},
"required": ["NAME", "DATE_OF_BIRTH"]
}
},
"fileRequirements": [
{
"fileName": "PROOF_OF_ADDRESS",
"required": false
},
{
"fileName": "ORG_STRUCTURE",
"required": false
}
],
"certificate": {
"id": "0cc3c5fe-fa88-4e79-b5eb-1c5194a19b08",
"certPem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tTUlJQmpUQ0NBVE9nQXdJQkFnSVVMaWk2Mk5KME0rdTZOTDZWV0hWRkhIZmJCWUl3Q2dZSUtvWkl6ajBFQXdJd0tqRVhNQlVHQTFVRUF3d09ZWEJwTG1OcGNtTnNaUzVqYjIweER6QU5CZ05WQkFvTUJrTnBjbU5zWlRBZUZ3MHlOVEF6TVRjeU1EQXdNVFJhRncweU5qQXpNVGN5TURBd01UUmFNQ294RnpBVkJnTlZCQU1NRG1Gd2FTNWphWEpqYkdVdVkyOXRNUTh3RFFZRFZRUUtEQVpEYVhKamJHVXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUmgyTTU0Q2FVMTlaWFRFaXZJVUNLOXluMmgvYld6Uno0bUhJWVE0ZzFYWnArdHRiM3Z6bGY2ZDQzYUhNYlRaQUpPTG1pbkdFZGwxbUZMdFRUTXdYb3ZvemN3TlRBekJnTlZIUkVFTERBcWdnNWhjR2t1WTJseVkyeGxMbU52YllJU2QzZDNMbUZ3YVM1amFYSmpiR1V1WTI5dGh3UUtBQUFCTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUExbksrNUxBUC9ueUlxRFlUaVVLYmlHNWYwTjVPUmFMb2Y1VXpXU0dsUEJBaUVBaEVOcDFxakRydG41aGFpMHdKeTNORzJKZ2xra084Y1QzellhN21mRTBiST0tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t",
"domain": "api.circle.com",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
}
}
```
### Step 2: Create the RFI response
You must provide all of the fields that are marked as required. Circle
recommends providing the fields marked as optional for the highest likelihood of
an approved RFI. If you do not have the requested information about the sender,
you should request it.
Once you have all of the required information, you can create the RFI response.
Format it into the appropriate JSON format, based on the schema provided in the
`fieldRequirements` parameter.
Before encrypting and sending the response, you should validate the JSON
response object against the JSON schema. See
[How-to: Integrate with JSON Schema](/cpn/guides/payments/integrate-with-json-schema)
for more information.
The following is an example RFI response:
```json JSON theme={null}
{
"ADDRESS": {
"city": "San Francisco",
"country": "US",
"postalCode": "94105",
"stateProvince": "CA",
"street": "123 Market Street"
},
"NAME": "Alice Johnson",
"DATE_OF_BIRTH": "1990-10-10",
"NATIONAL_IDENTIFICATION_NUMBER": "12-9483432",
"SOURCE_OF_FUNDS": "Company revenue",
"METHOD_OF_VERIFICATION": "Electronic"
}
```
### Step 3: Encrypt the RFI response
The RFI response must be encrypted before it is sent to the BFI. It is encrypted
in the same manner as the travel rule and beneficiary account data. For a full
guide on the encryption process, see
[How-to: Encrypt Travel Rule and Beneficiary Account Data](/cpn/guides/payments/encrypt-travel-rule-beneficiary-data).
### Step 4: Submit the RFI response
Submit the encrypted RFI response with the
[submit RFI data endpoint](/api-reference/cpn/cpn-platform/submit-rfi) . The
request should include the encrypted data and the version of the RFI JSON
Schema. The version should match the version in the `fieldRequirements`
parameter of the RFI object. An example request body is shown below:
```json JSON theme={null}
{
"rfi": {
"version": 1,
"data": "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJlcGsiOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiJ4ZzFRelVtaFRGdWV0Ti1Qd0N5dzdfRXd4SldKdk9ZTTFRLTNBRXFxZHdzIiwieSI6IjA0Z0dFMEsyT2ttVVZTMlhFUXZvR2hBUkJLWHQyWGRDRnRDNXpRMjQ3ajQifX0.Zaev1iHNNwG9_x0l3nwBcYlWBmlW9fP3.fmd2QQL_xKswDntW.1lYKjSoBZ_j7SKH4Q-kV8Zajcurh_zow1_e07zegfDNNEQ3DhsURPrjhDhyngtBe86T-WmDBM20j_1aChh1xwOS2vgKc-Kcv_4cNUzz0398ngYf2_xkEfvVckEexpX84omj5lmfqa0aleIQGldUVSuVV8fBl4YnH2oik5kDgvvQ4ap4MYhSTRqYJsi0bm6col7buPhnQJpojAjpp3ttoCYuOxbuDs5V_eNLIGuHPxF9KK7hS-l0qjLnNPcTnCbLL_fIveButXOzU54qB6lLssrE86O4xCH_d47_PAtaxJ296qtjGIB02dLSlrnORbVnvjrn17dhf8DmhkMy5GmFgtTs.TcOP8NKlgtgEaakORE1lXQ"
}
}
```
### Step 5: Upload required files
If the RFI includes a `fileRequirements` field, you must upload documentation
about the sender in addition to submitting the RFI response. Files must be
encrypted and uploaded one at a time to the
[upload RFI file endpoint](/api-reference/cpn/cpn-platform/upload-rfi-file). You
can upload files before or after submitting the RFI response, as long as it is
before the RFI decision is made.
See
[How-to: Encrypt Files for RFI Transmission](/cpn/guides/payments/encrypt-files)
for more information on how to encrypt files.
The [upload RFI file endpoint](/api-reference/cpn/cpn-platform/upload-rfi-file)
accepts a `multipart/form-data` request body in three parts. The request body
should include the encrypted file, the encryption details, and the file
metadata. The file metadata should include the filename, file type, and file
key. The file key should match the `fileKey` in the `fileRequirements` field of
the RFI object.
The following is an example request body:
```text Text theme={null}
------WebKitFormBoundary
Content-Disposition: form-data; name="fileMetadata"
Content-Type: application/json
{
"fileName": "example.pdf",
"fileType": "application/pdf",
"fileKey": "PROOF_OF_ADDRESS"
}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryption"
Content-Type: application/json
{
"encryptedAesKey": "",
"iv": ""}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryptedFile"; filename="encrypted_data.bin"
Content-Type: application/octet-stream
[BINARY FILE DATA]
```
Once you submit the RFI response, you receive the decision on the RFI via
webhook. If the BFI approves the RFI, you can continue with the payment, or
create a new payment for the sender if the existing payment has expired.
# Encrypt files for RFI transmission
Source: https://developers.circle.com/cpn/guides/payments/encrypt-files
This guide explains how to set up encryption and decryption between an OFI and a
BFI during the RFI process so that a file can be passed securely. This
encryption varies from the method used to encrypt JSON communications between
OFI and BFI, but shares some features. For compactness, files are encrypted
using AES, and then the key is encrypted using JWE. Both are then transmitted to
the BFI for a two-stage decryption process.
In the CPN system, the OFI encrypts a payload with a randomly generated AES key.
This key is then encrypted with the BFI's public key using
[JSON Web Encryption](https://datatracker.ietf.org/doc/html/rfc7516). The
encrypted AES key and encrypted file payload are transmitted to the BFI. The BFI
decrypts the AES key using their private JWK and uses it to decrypt the file
contents.
## Steps
The following sections describe the steps necessary to encrypt a file sent from
an OFI to a BFI through the RFI endpoint.
### Step 1: Generate a random 128-bit AES key
Using your chosen implementation language, generate a random 128-bit AES key for
AES-128-GCM encryption.
```java Java theme={null}
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
/**
* @return A SecretKey for AES encryption.
* @throws GeneralSecurityException if the AES algorithm is not available.
*/
public static SecretKey generateAesKey() throws GeneralSecurityException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128, new SecureRandom());
return keyGenerator.generateKey();
}
```
### Step 2: Generate a 12-byte IV
Using your chosen implementation language, generate a 12-byte IV.
```java Java theme={null}
import java.security.SecureRandom;
/**
* @return A 12-byte array containing the IV.
*/
public static byte[] generateIv() {
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
return iv;
}
```
### Step 3: Encrypt the file contents
Encrypt the file contents using AES-128-GCM using the key and IV from the
previous steps.
```java Java theme={null}
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.GeneralSecurityException;
/**
* @param plaintextPayload The raw data to encrypt.
* @param aesKey The AES key to use for encryption.
* @param iv The 12-byte Initialization Vector.
* @return The encrypted data, including the GCM authentication tag.
* @throws GeneralSecurityException if a cryptographic error occurs.
*/
public static byte[] encryptPayload(byte[] plaintextPayload, SecretKey aesKey, byte[] iv) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmParameterSpec);
return cipher.doFinal(plaintextPayload);
}
```
### Step 4: Encrypt the AES key
Using the JWK data from the quote response, encrypt the AES key that was used to
encrypt the file contents with the following parameters using JWE:
* Algorithm: ECDH-ES+A128KW
* Encryption method: A128GCM
```java Java theme={null}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEEncrypter;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import java.util.Base64;
import javax.crypto.SecretKey;
import java.security.interfaces.ECPublicKey;
/**
* @param aesKey The AES key to wrap.
* @param bfiPublicKey The recipient's Elliptic Curve public key.
* @return A compact, serialized JWE string representing the encrypted key.
* @throws JOSEException if an error occurs during JWE creation or encryption.
*/
public static String wrapAesKey(SecretKey aesKey, ECPublicKey bfiPublicKey) throws JOSEException {
// Create the JWEHeader using ECDH_ES+AS128KW and AES-128-GCM
JWEHeader header = new JWEHeader(
JWEAlgorithm.ECDH_ES_A128KW,
EncryptionMethod.A128GCM
);
// Base64 encode the AES key and wrap in JSON string
String base64AesKey = Base64.getEncoder().encodeToString(aesKey.getEncoded());
ObjectMapper objectMapper = new ObjectMapper();
String jsonPayload = objectMapper.writeValueAsString(base64AesKey);
Payload jwePayload = new Payload(jsonPayload);
JWEObject jweObject = new JWEObject(header, jwePayload);
JWEEncrypter encrypter = new ECDHEncrypter(bfiPublicKey);
jweObject.encrypt(encrypter);
return jweObject.serialize();
}
```
### Step 5: Transmit the encrypted payload
After performing the encryption steps from the previous steps, you should have
three components:
* The AES-encrypted file content
* The JWE string containing the encrypted AES key
* The base64-encoded 12-byte IV
Use these components to create a `multipart/form-data` request to the
[upload RFI file endpoint](/api-reference/cpn/cpn-platform/upload-rfi-file). A
`200` response from the API indicates that the encryption was performed
correctly and the BFI can decrypt the file's contents.
**Important:** Don't manually set the `Content-Type` header. Let your HTTP
client library set it automatically. The header must include a boundary
parameter like:
```text Text theme={null}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
```
If you manually set `Content-Type: multipart/form-data`, the request will fail.
```java Java theme={null}
import okhttp3.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
/**
* Upload an encrypted RFI file using multipart/form-data.
*
* @param paymentId The payment UUID.
* @param rfiId The RFI UUID.
* @param encryptedFile The encrypted file to upload.
* @param fileName The original file name.
* @param fileType The file type (e.g., "PDF", "JPEG").
* @param fileKey The file key (e.g., "ID_DOCUMENT", "PROOF_OF_ADDRESS_DOCUMENT").
* @param encryptedAesKey The JWE-wrapped AES key.
* @param iv The Base64-encoded initialization vector.
* @param accessToken The Bearer token for authentication.
* @throws IOException if the HTTP request fails.
*/
public static void uploadRfiFile(UUID paymentId, UUID rfiId, File encryptedFile,
String fileName, String fileType, String fileKey,
String encryptedAesKey, byte[] iv, String accessToken) throws IOException {
OkHttpClient client = new OkHttpClient();
ObjectMapper objectMapper = new ObjectMapper();
// Create data objects
FileMetadata fileMetadata = new FileMetadata(fileName, fileType, fileKey);
FileEncryption encryption = new FileEncryption(encryptedAesKey, Base64.getEncoder().encodeToString(iv));
// Build multipart request
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("fileMetadata", objectMapper.writeValueAsString(fileMetadata))
.addFormDataPart("encryption", objectMapper.writeValueAsString(encryption))
.addFormDataPart("encryptedFile", fileName,
RequestBody.create(encryptedFile, MediaType.parse("application/octet-stream")))
.build();
Request request = new Request.Builder()
.url(String.format("https://api.circle.com/v1/payments/%s/rfis/%s/files", paymentId, rfiId))
.header("Authorization", "Bearer " + accessToken)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Upload failed: " + response.code());
}
}
}
public static class FileMetadata {
public final String fileName;
public final String fileType;
public final String fileKey;
public FileMetadata(String fileName, String fileType, String fileKey) {
this.fileName = fileName;
this.fileType = fileType;
this.fileKey = fileKey;
}
}
public static class FileEncryption {
public final String encryptedAesKey;
public final String iv;
public FileEncryption(String encryptedAesKey, String iv) {
this.encryptedAesKey = encryptedAesKey;
this.iv = iv;
}
}
```
An example request body is shown below:
```text Text theme={null}
------WebKitFormBoundary
Content-Disposition: form-data; name="fileMetadata"
Content-Type: application/json
{
"fileName": "example.pdf",
"fileType": "application/pdf",
"fileKey": "PROOF_OF_ADDRESS"
}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryption"
Content-Type: application/json
{
"encryptedAesKey": "",
"iv": "",
}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryptedFile"; filename="encrypted_data.bin"
Content-Type: application/octet-stream
[AES ENCRYPTED BINARY FILE DATA]
```
# How-to: Encrypt travel rule and beneficiary data
Source: https://developers.circle.com/cpn/guides/payments/encrypt-travel-rule-beneficiary-data
This guide explains how to set up encryption and decryption between an OFI and a
BFI so that messages can be passed securely during payment flow. CPN uses
[JSON Web Encryption](https://datatracker.ietf.org/doc/html/rfc7516) (JWE) for
encrypting data in a secure and compact manner. This encryption is implemented
after a quote is accepted: the JWK data and public key for encryption are shared
in the quote response. The encryption is also used when communicating RFI data.
Both OFI and BFI can implement this encryption system in any programming
language that supports the required primitives. This encryption scheme is
required when performing the following tasks in CPN:
* **Creating a payment**: for encrypting the travel rule and beneficiary account
data
* **Submitting an RFI**: for encrypting the RFI JSON response
**Note:** If your RFI response includes a file, that file is
[encrypted in a different manner](/cpn/guides/payments/encrypt-files).
Regardless of the language used to implement the encryption, the following
parameters must be followed:
* Key agreement: ECDH-ES+A128KW
* Encryption method: A128-GCM
## Steps
The following sections describe the steps necessary to encrypt a message sent
from an OFI to a BFI. Note that this example uses beneficiary account data and
travel rule data, but the same encryption scheme applies for RFI data as well.
### Step 1: Retrieve the certificate from the quote response
Quote responses include a certificate field with required parameters to
establish encryption with the BFI. A sample (truncated) quote response is below:
```json JSON theme={null}
{
"data": [
{
"id": "6e4c7e85-39eb-4411-8c4a-683ff73846d6",
"paymentMethodType": "FPS",
"blockchain": "MATIC-AMOY",
"senderCountry": "US",
"destinationCountry": "HK",
"certificate": {
"id": "0cc3c5fe-fa88-4e79-b5eb-1c5194a19b08",
"certPem": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tTUlJQmpUQ0NBVE9nQXdJQkFnSVVMaWk2Mk5KME0rdTZOTDZWV0hWRkhIZmJCWUl3Q2dZSUtvWkl6ajBFQXdJd0tqRVhNQlVHQTFVRUF3d09ZWEJwTG1OcGNtTnNaUzVqYjIweER6QU5CZ05WQkFvTUJrTnBjbU5zWlRBZUZ3MHlOVEF6TVRjeU1EQXdNVFJhRncweU5qQXpNVGN5TURBd01UUmFNQ294RnpBVkJnTlZCQU1NRG1Gd2FTNWphWEpqYkdVdVkyOXRNUTh3RFFZRFZRUUtEQVpEYVhKamJHVXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUmgyTTU0Q2FVMTlaWFRFaXZJVUNLOXluMmgvYld6Uno0bUhJWVE0ZzFYWnArdHRiM3Z6bGY2ZDQzYUhNYlRaQUpPTG1pbkdFZGwxbUZMdFRUTXdYb3ZvemN3TlRBekJnTlZIUkVFTERBcWdnNWhjR2t1WTJseVkyeGxMbU52YllJU2QzZDNMbUZ3YVM1amFYSmpiR1V1WTI5dGh3UUtBQUFCTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUExbksrNUxBUC9ueUlxRFlUaVVLYmlHNWYwTjVPUmFMb2Y1VXpXU0dsUEJBaUVBaEVOcDFxakRydG41aGFpMHdKeTNORzJKZ2xra084Y1QzellhN21mRTBiST0tLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t",
"domain": "api.circle.com",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
}
}
]
}
```
### Step 2: Verify the certificate
**This step is only possible in the production environment. It will fail in the
sandbox environment.**
This step is strictly optional, however it is recommended as a best practice to
verify that the certificate is valid.
From the `certificate` object in the quote response, extract the `base64`
encoded format of the certificate from the `certPem` field, decode it, and
verify the following:
* The certificate is not expired
* The common name of the certificate is for a CPN BFI and matches the `domain`
field in the response
* The verifiable public key of the certificate matches the `jwk` field in the
response
```java Java theme={null}
public static void verify() throws Exception {
// Assume the API response is stored in a file "response.json"
String responseStr = new String(Files.readAllBytes(Paths.get("response.json")));
JSONObject response = new JSONObject(responseStr);
JSONObject certObj = response.getJSONArray("data").getJSONObject(0).getJSONObject("certificate");
// ---------- 1. Check certificate expiration ----------
String certPemB64 = certObj.getString("certPem");
byte[] pemData = Base64.getDecoder().decode(certPemB64);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream in = new ByteArrayInputStream(pemData);
X509Certificate certificate = (X509Certificate) cf.generateCertificate(in);
Date expirationDate = certificate.getNotAfter();
Date currentDate = new Date();
if (currentDate.after(expirationDate)) {
System.out.println("❌ Certificate has expired on " + expirationDate);
System.exit(1);
} else {
System.out.println("✅ Certificate is valid until " + expirationDate);
}
// ---------- 2. Verify that the certificate's common name (CN) matches the expected domain ----------
String certCN = getCommonName(certificate);
String expectedDomain = certObj.getString("domain");
if (certCN != null && certCN.equals(expectedDomain)) {
System.out.println("✅ Certificate common name matches expected domain.");
} else {
System.out.println("❌ Certificate common name does not match expected domain.");
System.exit(1);
}
// ---------- 3. Verify that the verifiable public key of the certificate is the same as the provided JWK ----------
JSONObject providedJWK = certObj.getJSONObject("jwk");
String xProvided = providedJWK.getString("x");
String yProvided = providedJWK.getString("y");
PublicKey pubKey = certificate.getPublicKey();
if (pubKey instanceof ECPublicKey) {
ECPublicKey ecPub = (ECPublicKey) pubKey;
ECPoint point = ecPub.getW();
String xCert = toBase64URL(point.getAffineX(), 32);
String yCert = toBase64URL(point.getAffineY(), 32);
if (xCert.equals(xProvided) && yCert.equals(yProvided)) {
System.out.println("✅ Certificate public key matches provided JWK.");
} else {
System.out.println("❌ Certificate public key does not match provided JWK.");
System.exit(1);
}
} else {
System.out.println("❌ Certificate public key is not EC type.");
System.exit(1);
}
}
```
### Step 3: Extract and create the JWK
From the `certificate` field, extract the JWK parameters: `kty`, `crv`, `kid`,
`x`, `y` and create the JWK object in your code using a suitable library (for
example, Nimbus JOSE+JWT in Java).
```java Java theme={null}
import com.nimbusds.jose.jwk.JWK;
public static JWK parseJwkFromJson() throws ParseException {
String jwkJson = """
{
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
""";
JWK jwk = JWK.parse(jwkJson);
return jwk;
}
```
### Step 4: Prepare the payload
Create the payload for travel rule data and beneficiary account data. Get the
required fields from the
[requirements endpoint](/api-reference/cpn/cpn-platform/get-payment-requirements)
and construct them to the correct format.
The correct format for travel rule data and beneficiary account data is a JSON
array of objects where each object contains two properties: `name` and `value`.
Whether an object is required to be present is defined by the `optional` field
in the object returned by the requirements endpoint.
An example of each is shown below:
**travelRuleData**
```json JSON theme={null}
[[
{
"name": "BENEFICIARY_ADDRESS",
"value": {
"city": "San Francisco",
"country": "US",
"postalCode": "94105",
"stateProvince": "CA",
"street": "123 Market Street"
}
},
{
"name": "BENEFICIARY_NAME",
"value": "Alice Johnson"
},
{
"name": "ORIGINATOR_ACCOUNT_NUMBER",
"value": "9876543210"
},
{
"name": "ORIGINATOR_ADDRESS",
"value": {
"city": "New York",
"country": "US",
"postalCode": "10001",
"stateProvince": "NY",
"street": "456 Madison Avenue"
}
},
{
"name": "ORIGINATOR_FINANCIAL_INSTITUTION_ADDRESS",
"value": {
"city": "Chicago",
"country": "US",
"postalCode": "60603",
"stateProvince": "IL",
"street": "789 Apple Drive"
}
},
{
"name": "ORIGINATOR_FINANCIAL_INSTITUTION_NAME",
"value": "First National Bank"
},
{
"name": "ORIGINATOR_NAME",
"value": "Robert Smith"
}
]
```
**beneficiaryAccountData**
```json JSON theme={null}
[
{
"name": "BANK_NAME",
"value": "Test Bank"
},
{
"name": "RECIPIENT_ADDRESS",
"value": {
"street": "123 Test St",
"city": "Sacramento",
"state": "CA"
}
},
{
"name": "RECIPIENT_CITY",
"value": "Sacramento"
}
]
```
### Step 5: Encrypt the payload
Convert the data from the previous step into a JSON string, then use the JWK to
encrypt it. Ensure that the following parameters are used:
* Algorithm: ECDH-ES+A128KW
* Encryption method: A128GCM
```java Java theme={null}
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEEncrypter;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import com.nimbusds.jose.jwk.JWK;
/**
* Encrypts a payload using JWE (JSON Web Encryption) with ECDH-ES key agreement.
*
* This method creates a JWE compact string with the provided payload and encrypts it using
* the recipient's public key. The encryption algorithm used is ECDH-ES with AES-128 key wrap,
* and the content encryption method is AES GCM with 128-bit key.
*
* @param the type of the payload to encrypt
* @param payload the data to be encrypted
* @param recipientJwk the recipient's JWK (JSON Web Key) containing the public key for encryption
* @return the serialized JWE in compact form
* @throws JOSEException if an error occurs during the encryption process
*/
public static String encrypt(T payload, JWK recipientJwk) throws JOSEException {
String plainText = JsonUtils.toJson(payload);
// Create the JWEHeader using ECDH_ES+AS128KW and AES-128-GCM
JWEHeader header = new JWEHeader(
JWEAlgorithm.ECDH_ES_A128KW,
EncryptionMethod.A128GCM
);
// Create the JWE object with the payload to encrypt
JWEObject jweObject = new JWEObject(
header,
new Payload(plainText)
);
// Create an encrypter with the recipient's public key
JWEEncrypter encrypter = new ECDHEncrypter(recipientJwk.toECKey());
// Encrypt the JWE
jweObject.encrypt(encrypter);
// Return the serialized JWE string in compact form
return new jweObject.serialize();
}
```
### Step 6: Send and verify the encrypted payload
Send the encrypted payload in the API request to
[create a payment](/api-reference/cpn/cpn-platform/create-payment). The
preceding example uses version 1 for `beneficiaryAccountData` and
`travelRuleData` fields.
The API returns a `200` response if the data is properly encrypted and can be
decrypted by the BFI, otherwise an
[encryption-related error code](/api-reference/cpn/error-codes#encryption) is
returned.
# Integrate with JSON Schema
Source: https://developers.circle.com/cpn/guides/payments/integrate-with-json-schema
To facilitate the effective passing of RFI data, CPN provides
[JSON Schema](/cpn/concepts/api/json-schema) that gives a clear,
machine-readable contract you can validate your response against. This guide
provides information about how to integrate with the JSON Schema returned by the
[get details for an RFI endpoint](/api-reference/cpn/cpn-platform/get-rfi).
## Steps
Use the following steps to retrieve JSON Schema for a given quote and respond
with the required information.
### Step 1: Get the JSON Schema
Call the [get details for an RFI](/api-reference/cpn/cpn-platform/get-rfi)
endpoint. The API returns a JSON Schema object that defines the fields that must
be transmitted in the RFI response.
### Step 2: Build a JSON response
Using the schema as a blueprint, construct your RFI data as a JSON object. For
example, if the schema requires the `address` field, you should create a nested
JSON object for the address, as defined by the `properties` and `$defs` in the
schema.
### Step 3: Validate the response
Before encrypting the response for transit, CPN expects that you perform
client-side validation on the data. Using a JSON Schema validation library,
check your constructed JSON objects against the schemas from Step 1. This should
catch any structural or formatting errors before you interact with the CPN API.
### Step 4: Encrypt the response
Once validated, convert your JSON objects to strings and encrypt them. The steps
outlined in
[How-to: Encrypt Travel Rule and Beneficiary Account Data](/cpn/guides/payments/encrypt-travel-rule-beneficiary-data)
also apply to encrypting RFI data for transit.
### Step 5: Transmit the payload
Send the encrypted payload to the
[submit RFI data endpoint](/api-reference/cpn/cpn-platform/submit-rfi).
The following is an example payload:
```json JSON theme={null}
{
"rfi": {
"version": 1,
"data": "eyJhbGciOiJFQ0RILUVTK0ExMjhLVyIsImVuYyI6IkExMjhHQ00iLCJlcGsiOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiI1TEtTbUtsNmVYV0dQNHlGb2tVaks4RGppU0l5YWpQdzJ6UUp1YVhFbEZzIiwieSI6IlRVT29MTV8wc294b0UzYVFmSExlUzRlRkZ3RU9tZ1VDRUh1eDVRMXdVYTQifX0.kVGnfB8eIqxq3bMvhedxUmZvkCrRfQOy.bTUOc_ilvZjb9VYP.pOS6Ul8Jnp_pijWtaQYC0b1NLN1Nc-t_MTsfj5EZy6s62ijQWGtAoo3enwx3gOpXIQDIIF7c-F3KSrVO33a5RIF8a4tvU7pRk_JDKVgHFVpSzvOaUHNHsMUvvctAncx1SneVE_gnm5ATkWO4_1i7irTkb-cjWLmvJ1vVEyJiC9ZgsEdK_eBAewpZTyyKxvoxBpo8IZ3g7qax712TvMsNpIJ_faQmsyRKwt2kBxDPyrWILcIvS1qij6uVOsEP2R6LXbWfBqVbklsgNPWjndXSmOJmsTdBjFzublMLSHmLxkfaD9gt3DxgD16rqDlKO9KmaRF3r322VumP4hrlfOsZUHHcVGeTpgjZwI9jrj76PiYtyCOeR7-kz97PamqrOQAfWRE8Rcp8QdCO6yFHFe3N2pI5kWakH783DVDAsm_x2VY_V8vCDe-xkIoJOabx3LqaxHq8_x_ATXZ01NX-5F9VqgtE73FOIGhinxt8G-Kfxvdlx6gv-QSfBvigVuoR1OCSPNB3OHirEKXY3IjWnjZuENNnXl9CqrCsHQZS1eb1KcaIkg_2NF6gi54htX6ijpUtyXS03IGZW6Bn6CSWqmmvMStIAIYO-8ZJrrleeCfU6WY5KjFlIvN6th-tE41JNJWS9mVqhjzwPC7GQ5bGkqq2zJKhALOuN_0oG4jXOYo1s3YAn-xw3kpq46fYyUrX4nLSocaKDeKOMMqh57_jcMDmHrXdmjlHGPMrBPdU_deub121c3ofTKgJg4KRC8mcTYcXe71uflARAr24msd9A_IPPVbhfvs06HOVKL2-jK6r_WL_9vWU3q-OPQHU3eWUOhI18vZ0CkGqJjtEKyBBmc4inWEbrsO02P2Zd5VTBCIoAaNrpY6n5mNyKPo6-PyrxNy27d6u2yoZvjG6QSuUO9DaX4AyGsE87agh_XgLW9RPXfcZQ7F5aUCQwEA0cF9YSEaYYBGziS7oQ99D1th1MQqEPcFxD5YPdyaPykU3Zbf5T8HSZz1risMbgQu97eXoarxm801pdw5cuKijqj39BVrgdLUfaQ9P-iGWcVJk8ZVXfHK3JHGaBuIjYNIy5lHF3TsKgV2x-wWxqER9vPdgejAGirkxHQaKPt2rg0rmbk3R7YTibT7yTKwO1PRiewi0lxKWvZqDbCoedyuYZCR0vup4S1ZVYX6uGBn-F5ODrnLoAZTbnV1tjDJgxFpA8EKsXWTrc78XH0715lY2OnbDtBOLMbxFJjK1_YDRU2UMkZTEx5BrhR4YibBd4-BCZT-dWTxanOHMxTKl823tyggY5lBQ8DlsLj1qZpOxKWYTM8WrLCZa7vZo9FmXw38FxYrqbwespe7xdS7onfmlI4Ug92eTtZ31FbZfcqOiAGgH9Etkj1M9scP0is6zVC6cIUQzC3NmHNxNtPv6XjMUWigijosvs7x9-qyVCqzJZlEGck2BJ6Uk7ug9QKcxHtaWgtuHPnA77KVE3Vi_dikYaXzxIdgjrGtqqUhXEhrAFkesvJ0UeH1ac94e-R8UrJX75w4DDoRyaFxQhDFdHVtD0ceVN7ekYJkmoS2eSAzWDwsKUvUrmafcfpQ922tZGq0RvnMh39SWy6I6xRFyYHarra24Gbawa5_THjB8TkHJ180giPC3KQAa_6Vz8CP-fks6_-MdGXPW0SYhNghNi_14ZXQ3rZYHaTICgeHhBfhJ4MQ6OlRkiRCrIkM5WSOTS_H715aeyUCL-dlZ0TiG2DpAIXkN1KMRp0jL_W12pg5SjKFAlOGnQgkgj9rO_C7pLxMyIhgEUKPHXMGN6Q7tpVhAAyTl0tabd7cvyWnf5L4UiyIpvb1qrt1bY6-VNA4KXz6Y5uiiBYFF_FIN3K6YZCT4l0zuh_gxmP9eba7zM5kgas1tr1IxYWI-mQiE9Rb8rX0hqXpMj27h9Mf_Sn56TJeEmW8d8x2Xy2Nb7KU9vQO-6uc_taXP0Lv5LFcSwZkdVe7FOeZD-GoaNDly9MdhXI4ELP4MOZXZFJeN7aCJvJeLRBPWnoM2aazDwtlA_KfhZVFtCsKKjJazv6J9h6CDHAm7NuexVJiPdtS1yknQDWrIBfneaLJasmtuhuAbRPtiTvvhQOXt51q0G7sjCSmmeNMwb5fFphe4wLJNmsbdgMFDcUVzb0ImIArCxqBEEK8uuRGcYaRifaLv0zkk29NAT99Ux580zaeN_x7aIMpJTt7jLP0-HZ-QU3MAGg3PLJ3_Oly6ABoULkEkmRhF0TtpU8z-kmmN6FtmzZK74kYEC75smgVTnnJK1kBdsZw9cxHeUx8aDJGrS3OK-uYN0dPJls2LC7X1rHD7ES179nPVBeJA2REm-jSlhVlV_I7JrVzlcXs0Fhx0hX-OgMKz_yQNHnM6RbgGnSLDcvND7vXqkEMgHeDRomhU1hMeu0DymctTKawtzIkV7MYwoibLDUeMvaXN8MicMs64D8I3Ld7BLtFf9o8rMeeNJ0Om7xdl9SGD2RYAep0cxw2rEIIr859e1vutBuvrrMz09dZKd05t1lu0jRztkQzUW9N-VZh9muTAoh_s3NrF2ZTFnRAfzxohwNLxxmX-Ov94FXBhuRHBQXb931I5m2URglrYcoAZ8RFoiAWW2AkHcyb5PRoQIt-LQhOM-bcn1VB44Okh10jqWpGbLno6AvE5FcoRVcspb0tor9EbksW2cipoypcaat5NhQgvo2rAPmkHGi8iB-z_xWORvUkaA2xknNW59myO6qTvjYQ_1P1ESbRO30nUeqgP3VoZrC1-bi9IbgSi-na31xEhdr-ZGuvtRSzyhWwqfviJgSMZ6_kkGW3gUI4ldurTwss6gDHWYmdV-lHtPMfYuD2KMQk1EyJ-3vZx0syLKDACL37HqAYvkW5GiOxrK9cNrSInQ8rZMDkXsaCZMEtvs03Qw2DyCBdoFeXPvFHJ6Vetou-7Oo6rdlmSkt5KdA-w9KKVIAt9WtYxNc-wjFm-vOMcOHb_xxzacDzYFJj8NxdW8GaVdrCvh3j5Yakr_3vSvRM5rpzQKMqw-B53SWV3oOHYu29qso5zSTz-dvAlZUL8Z1a3s13qdwhUkkRexIi6oTU0Da21pT-gCan2jSY_VKR8JdirKTLocn74JZ3InF8iH8XhOD0X7ySIbB69HqMLYRd9r13q2odfJ3cN1CWSF71zgIuZoYSbkO9DeFvCImA-WEIWrP1kJiYlpJi8VnFNQs8XWIxdCydft-8zvoXZbi0MoNt3kJExbKx7dReHx6B-6iwjuW8-o5RbeMvdTIYTp-y6YPsFhcVCvTlS7IefXqG4G3OrnNc5JIVnvdYXWCWUdWguiKdqB6FL_LSWOtsWrB6nIv69WP1WJJSa0Oa_EkxEwXK1BGH9-QpMqkP6rATSvsbaTywfCxqUqm7SzYH81lSCIvI5J7bWX5blk9rNZbp3aSeEpVRm03vwLrgUHLv9vMAopNnw5fZGKfcg3I5ZWYHNQo5BZk9LVpebMBYGijwNDnuFu08eF7ShlEzy50NsvgeDxZtfUVx8cviYn0MoTJBPlz1mXRSIr3zDIpefxDGnsQ1KK8LTi0Gbnt_ybeiT4yTmajrPLE4566D6AQIdDmCg_RsBtB_lAw.S2qu0MNMu2WNiEYoHh25ww"
}
}
```
# Request payment configurations and routes
Source: https://developers.circle.com/cpn/guides/payments/request-payment-config
You can use the API to discover which corridors, currencies, and payment methods
are available. Retrieving this information and handling it in your integration
allows you to show your users the available sources and destinations for funds
in CPN.
## Steps
Use the following steps to request CPN configuration overview, and get the
available routes from country to country.
### Step 1: Request configuration overview
Make a request to the
[configurations overview endpoint](/api-reference/cpn/cpn-platform/get-payment-configurations-overview)
to receive a list of supported currencies, countries, payment methods, and
blockchains.
```shell Shell theme={null}
curl -H "Authorization: Bearer ${YOUR_API_KEY}" \
-X GET https://api.circle.com/v1/cpn/v1/ofi/configurations/overview
```
**Response**
```json JSON theme={null}
{
"data": {
"sourceCurrencies": ["USDC"],
"destinationCountries": ["BR", "HK", "MX", "NG", "US"],
"destinationCurrencies": ["BRL", "HKD", "MXN", "NGN", "USD"],
"paymentMethodTypes": [
"ACH-BANK-TRANSFER",
"BANK-TRANSFER",
"CHATS",
"FPS",
"PIX",
"SPEI",
"WIRE"
],
"blockchains": ["ETH-SEPOLIA", "MATIC-AMOY", "SOL-DEVNET"]
}
}
```
### Step 2: Request available route details
Evaluate the available route details for a specific transfer by making a request
to the
[supported payment routes endpoint](/api-reference/cpn/cpn-platform/list-routes).
This example evaluates a route from the US to Mexico, but other routes can be
evaluated by changing the query parameters. The endpoint accepts an optional
`transactionVersion` query param (`VERSION_1`, `VERSION_2`); with `VERSION_2`, a
chain-specific buffer is added to the crypto min limit to cover fees (for
example, gas fee), paid in USDC from the source amount.
```shell Shell theme={null}
curl -H "Authorization: Bearer ${YOUR_API_KEY}" \
-X GET https://api.circle.com/v1/cpn/v1/ofi/configurations/routes?sourceCurrency=USDC&destinationCountry=MX
```
**Response**
```json JSON theme={null}
{
"data": [
{
"destinationCurrency": "MXN",
"paymentMethodType": "SPEI",
"blockchain": "ETH-SEPOLIA",
"fiatLimit": {
"min": "168",
"max": "837500",
"currency": "MXN"
},
"cryptoLimit": {
"min": "8.34",
"max": "41552.14",
"currency": "USDC"
}
}
]
}
```
# Create an onchain transaction
Source: https://developers.circle.com/cpn/guides/transactions/create-an-onchain-txn
When a payment is in the `CRYPTO_FUNDS_PENDING` state, the OFI must initiate an
onchain transaction to fulfill the payment in USDC. CPN provides an API to help
prepare and validate the onchain transaction call data, and to broadcast the
transaction and monitor its state.
This guide describes the
[Transactions V2](/cpn/concepts/transactions/transactions-v2) flow. Note that
your quote must be created with the `transactionVersion` parameter set to
`VERSION_2` to match the API that you're using.
## Prerequisites
Before you begin, ensure that you have:
* Created a quote through the CPN API with the `transactionVersion` parameter
set to `VERSION_2`.
* USDC in your sender wallet
* (EVM chains only) Granted a USDC allowance to the `Permit2` contract. See
[How-to: Grant USDC Allowance to Permit2](/cpn/guides/transactions/grant-usdc-allowance-to-permit2)
for more information.
* (Solana only) Ensure your Solana account has been initialized and funded.
Gas fees are not charged in native tokens, but in USDC. They are determined at
quote creation instead of transaction creation in the quote `fees` field. The
gas fee is collected by Circle's payment settlement smart contract during
onchain transaction processing.
## Steps
Use the following steps to create and broadcast a USDC transfer to the
blockchain:
### Step 1: Prepare the call data
Call the
[create transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint to get an unsigned message object. Note that transaction objects differ
between EVM blockchains and Solana.
The API call must include the sender wallet information (which must be an EOA
wallet). The rest of the transaction data such as amount, chain, destination
address, and other information is populated automatically by CPN using the
payment record.
### Step 2: Create and sign the transaction
Review the unsigned message object and ensure that it matches your expectations
for the crypto transaction. Depending on the blockchain, the signing process
varies.
#### EVM
When the unsigned data has been confirmed, you must sign the transaction payload
from the API endpoint using [EIP-712](https://eips.ethereum.org/EIPS/eip-712)
typed data signing from your sender wallet. The following is an example payload:
```json theme={null}
{
"type": "PAYMENT_SETTLEMENT_CONTRACT_V1_0_PAYMENT_INTENT",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"PaymentIntent": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "validAfter",
"type": "uint256"
},
{
"name": "validBefore",
"type": "uint256"
},
{
"name": "nonce",
"type": "bytes32"
},
{
"name": "beneficiary",
"type": "address"
},
{
"name": "maxFee",
"type": "uint256"
},
{
"name": "requirePayeeSign",
"type": "bool"
},
{
"name": "attester",
"type": "address"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "PaymentIntent"
}
]
},
"domain": {
"name": "Permit2",
"chainId": "11155111",
"verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
"message": {
"nonce": "25668617285137697861288274946631174355105919960416755114569514179393151588120",
"spender": "0xe2B17D0C1736dc7C462ABc4233C91BDb9F27DD1d",
"witness": {
"to": "0xc75c3e371d617b3e60db1b6f3fa2f0689562e5a7",
"fee": "0",
"from": "0x57414adbBbc4BBA36f1dE26b2dc1648b28ae7799",
"nonce": "0x38bfec2b230187932870d575132e8ae1f83b34c10e3bf6d64c377f0c13245718",
"value": 14174474,
"attester": "0x768919ef04853b5fd444ccff48cea154768a0291",
"validAfter": "1757358106",
"beneficiary": "0x4f1c3a0359A7fAd8Fa8E9E872F7C06dAd97C91Fd",
"validBefore": "1757361726",
"requirePayeeSign": false
},
"deadline": "1757362866",
"permitted": {
"token": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"amount": "14174474"
}
},
"primaryType": "PermitWitnessTransferFrom"
}
```
After signing the payload, you should have an EIP-712 data signature in hex
string format.
#### Solana
The transfer data to be signed is encoded in the `encodedMessageToBeSigned`
field from the
[POST /v2/cpn/payments/:paymentId/transactions](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint. You would need to first decode it using the `base64` library and then
sign it using a Solana library with your wallet key pair.
```typescript theme={null}
import { Keypair, Transaction, Message } from "@solana/web3.js";
import bs58 from "bs58";
function signTransaction(
encodedMessageToBeSigned: string,
keypair: Keypair,
): string {
// 1. Decode base64 message
const messageBytes = Buffer.from(encodedMessageToBeSigned, "base64");
// 2. Deserialize as Solana Message
const message = Message.from(messageBytes);
// 3. Create transaction with empty signatures
const SIGNATURE_LENGTH = 64;
const EMPTY_SIGNATURE_BASE58 = bs58.encode(
Buffer.alloc(SIGNATURE_LENGTH).fill(0),
);
const numSignatures = message.header.numRequiredSignatures;
const transaction = Transaction.populate(
message,
Array(numSignatures).fill(EMPTY_SIGNATURE_BASE58),
);
// 4. Sign with keypair
transaction.partialSign(keypair);
// 5. Serialize signed transaction, which you can then submit to the
// POST /v2/cpn/payments/:paymentId/transactions/:transactionId/submit endpoint
const signedTransaction = transaction
.serialize({ requireAllSignatures: false })
.toString("base64");
return signedTransaction;
}
```
If you are using the Circle Wallets, you can use the
[sign transaction](/api-reference/wallets/developer-controlled-wallets/sign-transaction)
endpoint to sign the transaction object.
```typescript theme={null}
import { Keypair, Transaction, Message } from "@solana/web3.js";
import bs58 from "bs58";
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
async function signTransactionWithCircleWallet(
encodedMessageToBeSigned: string,
walletId: string,
apiKey: string,
entitySecret: string,
): Promise {
// 1. Decode base64 message
const messageBytes = Buffer.from(encodedMessageToBeSigned, "base64");
// 2. Deserialize as Solana Message
const message = Message.from(messageBytes);
// 3. Create transaction with empty signatures
const SIGNATURE_LENGTH = 64;
const EMPTY_SIGNATURE_BASE58 = bs58.encode(
Buffer.alloc(SIGNATURE_LENGTH).fill(0),
);
const numSignatures = message.header.numRequiredSignatures;
const transaction = Transaction.populate(
message,
Array(numSignatures).fill(EMPTY_SIGNATURE_BASE58),
);
// 4. Serialize transaction
const rawTransaction = transaction
.serialize({ requireAllSignatures: false })
.toString("base64");
// 5. Initialize Circle client
const circleClient = initiateDeveloperControlledWalletsClient({
apiKey: apiKey,
entitySecret: entitySecret,
});
// 6. Sign transaction, which you can then submit to the
// POST /v2/cpn/payments/:paymentId/transactions/:transactionId/submit endpoint
const signedTransaction = (
await circleClient.signTransaction({
walletId: walletId,
rawTransaction: rawTransaction,
})
).data.signedTransaction;
return signedTransaction;
}
```
### Step 3: Submit the signed transaction
Call the
[submit transaction V2](/api-reference/cpn/cpn-platform/submit-transaction-v2)
endpoint to submit the signed transaction data to CPN. CPN broadcasts the
transaction and provides a webhook to notify you of the transaction status. This
webhook is also provided to the BFI.
Once the BFI confirms that they have received the desired amount for the
payment, the BFI initiates the fiat payment.
#### Handling failures
If a transaction fails and the payment is still valid (for example, it has not
expired), you can address the issues with the transaction and initiate a new
transaction. Because gas fees in native tokens are paid from a Circle-controlled
wallet, insufficient gas is unlikely to be an issue. You can use a similar
approach to address other onchain failures.
Only one active transaction is allowed at a time per payment, so you can only
initiate a new transaction once the previous one has failed.
# How-to: Grant USDC allowance to Permit2
Source: https://developers.circle.com/cpn/guides/transactions/grant-usdc-allowance-to-permit2
Grant a USDC token allowance to the Permit2 contract using a Circle Wallets developer-controlled wallet or an EIP-1193 Ethereum wallet
For Transactions V2 on EVM blockchains, there is a dependency on the `Permit2`
contract to enable allowance management. To get the benefits of Transactions V2,
you must grant a USDC token allowance to the `Permit2` contract.
This guide shows two examples of how to grant a USDC token allowance to the
`Permit2` contract. The
[`Permit2` documentation](https://docs.uniswap.org/contracts/permit2/overview)
provides additional examples of how to grant this allowance.
## Prerequisites
The examples on this page show how to grant a USDC token allowance to the
`Permit2` contract using a
[Circle Wallets developer-controlled wallet](/wallets/dev-controlled) or a
generic EIP-1193 Ethereum wallet. Before you begin, ensure you have:
* If you are following the Circle Wallets example:
* A Circle Developer Account
* A
[developer-controlled wallet](/wallets/dev-controlled/create-your-first-wallet)
* **Node.js** and **npm** installed on your development machine
* A project set up as described in the below section
### Set up your project
1. Initialize a new Node.js project and install dependencies:
```shell theme={null}
npm init -y
npm pkg set type=module
npm install viem dotenv
```
2. In the project root, create a `.env` file and add the following variables:
```shell theme={null}
USDC_CONTRACT_ADDRESS=
PERMIT2_CONTRACT_ADDRESS=
APPROVAL_AMOUNT=
WALLET_ADDRESS=
```
The `PERMIT2_CONTRACT_ADDRESS` is the same across all EVM blockchains
(`0x000000000022D473030F116dDEE9F6B43aC78BA3`), but you should verify it with
the blockchain explorer on the chain you are using. You can find the
`USDC_CONTRACT_ADDRESS` on the
[USDC contract address page](/stablecoins/usdc-contract-addresses).
The USDC token has 6 decimals. To approve \$100 USDC, set `APPROVAL_AMOUNT` to
`100000000` (100 \* 106).
If you are following the Circle Wallets example, you will also need to add
the following variables:
```shell theme={null}
CIRCLE_WALLET_ID=
CIRCLE_WALLETS_API_KEY=
ENTITY_SECRET=
```
If you are following the EIP-1193 Ethereum wallet example, or your Circle
Wallet is on the generic `EVM` / `EVM-TESTNET` chain, you will also need to
add the following variable:
```shell theme={null}
RPC_URL=
```
3. Create an `index.js` file. You'll add code step by step in the following
sections.
### Grant a USDC token allowance to the `Permit2` contract
The following example code shows the process for granting a USDC token allowance
to the
[`Permit2` contract](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3)
using a Circle Wallets developer-controlled wallet or an EIP-1193 Ethereum
wallet.
This example is for a Circle Wallets developer-controlled wallet on specific EVM
blockchains (for example, `ETH`, `ETH-SEPOLIA`, `MATIC`, `MATIC-AMOY`, etc.). If
your Circle Wallet is on the generic `EVM` / `EVM-TESTNET` chain, use the
example in the "Circle Wallets (generic EVM)" tab.
```javascript theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
import { randomUUID } from "crypto";
import dotenv from "dotenv";
dotenv.config();
/**
* Approves a specified amount of USDC for the Permit2 contract using
* a developer-controlled wallet managed by Circle.
*
* This function sends an on-chain transaction to call the ERC-20 `approve`
* method on the USDC contract, allowing the Permit2 contract to spend
* the specified amount on behalf of the wallet.
*
* @async
* @function approveUSDCWithCircleWallets
* @returns {Promise
This example is for a Circle Wallets developer-controlled wallet on generic EVM
blockchains (for example, `EVM`, `EVM-TESTNET`). You should create
chain-specific Circle Wallets (for example, `ETH`, `ETH-SEPOLIA`, `MATIC`,
`MATIC-AMOY`) instead of a generic EVM wallet and follow the example in the
"Circle Wallets" tab instead. If you're migrating an existing integration, see
[How-to: Migrate from transactions V1 to V2](/cpn/guides/transactions/migrate-from-txn-v1-to-v2).
```javascript theme={null}
import { createPublicClient, http, encodeFunctionData, erc20Abi } from "viem";
import { sepolia } from "viem/chains";
import dotenv from "dotenv";
dotenv.config();
/**
* Signs a transaction using a developer-controlled wallet managed by Circle.
*
* For EVM chains, accepts a transaction object in JSON format.
* The transaction object will be automatically stringified if needed.
*
* @async
* @function signTransaction
* @param {object} transaction - Transaction object for EVM chains
* @returns {Promise
```javascript theme={null}
import {
createWalletClient,
createPublicClient,
http,
custom,
erc20Abi,
} from "viem";
import { sepolia } from "viem/chains";
import dotenv from "dotenv";
dotenv.config();
/**
* Approves a specified amount of USDC for the Permit2 contract using
* a custom wallet with an EIP-1193 provider (like MetaMask).
*
* This function sends an on-chain transaction to call the ERC-20 `approve`
* method on the USDC contract, allowing the Permit2 contract to spend
* the specified amount on behalf of the wallet.
*
* @async
* @function approveUSDCWithEIP1193Wallet
* @param {any} [provider] - EIP-1193 provider.
* @returns {Promise} The transaction hash and receipt.
*/
export async function approveUSDCWithEIP1193Wallet(provider) {
const publicClient = createPublicClient({
chain: sepolia,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account: process.env.WALLET_ADDRESS,
chain: sepolia,
transport: custom(provider),
});
const hash = await walletClient.writeContract({
address: process.env.USDC_CONTRACT_ADDRESS,
abi: erc20Abi,
functionName: "approve",
args: [
process.env.PERMIT2_CONTRACT_ADDRESS,
BigInt(process.env.APPROVAL_AMOUNT),
],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
return { hash, receipt };
}
/* -------- Example usage with EIP-1193 wallet ---------
// Refer to https://viem.sh/docs/clients/transports/custom and your wallet provider's documentation for the provider object.
const {hash, receipt} = await approveUSDCWithEIP1193Wallet({provider: window.ethereum});
console.log('Hash:', hash);
console.log('Receipt:', receipt);
---------------------------------- */
```
# How-to: Migrate from transactions V1 to V2
Source: https://developers.circle.com/cpn/guides/transactions/migrate-from-txn-v1-to-v2
Update your CPN integration from Transactions V1 to V2
Transactions V2 allows for a more straightforward integration with CPN compared
to Transactions V1. It also provides more powerful onchain settlement
primitives, such as auto-acceleration and gas fees that are fixed at quote time.
If you have previously integrated with CPN using the Transactions V1 API, you
can migrate to Transactions V2 without changes to your wallet infrastructure.
Compared to Transactions V1, Transactions V2 has the following differences:
* When creating the CPN quote, you must set the `transactionVersion` parameter
to `VERSION_2`.
* Gas fees are not charged in native tokens, but in USDC. They are determined at
quote creation instead of transaction creation in the quote `fees` field. The
gas fee is collected by Circle's payment settlement smart contract during
onchain transaction processing.
* There are separate API endpoints for creating and submitting transactions.
Depending on the blockchain, there are additional differences:
* Before initiating a transaction, the sender wallet must grant the
[`Permit2` contract](https://docs.uniswap.org/contracts/permit2/overview) a
USDC allowance.
* When signing the transaction, you no longer need to compose and sign the raw
EVM transaction; you only need to sign the EIP-712 typed data provided to you
in the `messageToBeSigned` field and submit the signature.
* CPN handles the auto-acceleration of the transaction. You no longer need to
monitor transaction broadcast status or submit acceleration transactions.
* In the signing process, instead of deserializing and signing the
`messageToBeSigned` field, you would need to use the
`encodedMessageToBeSigned` field. Please refer to [How-to: Create an Onchain
Transaction](/cpn/guides/transactions/create-an-onchain-txn) for more
information on how to sign the transaction in V2.
This guide provides information on how to migrate your existing integration from
Transactions V1 to V2 on EVM. On Solana, you would follow a similar process with
some differences in the signing process.
## Prerequisites
Before you begin, ensure that you have:
* Obtained an API key for CPN from Circle
* USDC in your sender wallet
* cURL installed on your development machine
* (EVM chains only) Granted a USDC allowance to the `Permit2` contract. See
[How-to: Grant USDC Allowance to Permit2](/cpn/guides/transactions/grant-usdc-allowance-to-permit2)
for more information.
> **Note:** This guide provides API requests in cURL format, along with example
> responses.
## Steps
Use the following steps to create an onchain transaction using Transactions V2.
### Step 1. Create a quote
Use the [create a quote](/api-reference/cpn/cpn-platform/create-quotes) endpoint
to create a quote with the `transactionVersion` parameter set to `VERSION_2`.
When checking limits beforehand, call the
[list routes](/api-reference/cpn/cpn-platform/list-routes) endpoint with
`transactionVersion=VERSION_2` so returned crypto min limits include the
chain-specific fee buffer.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/cpn/quotes \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"paymentMethodType": "SPEI",
"senderCountry": "US",
"destinationCountry": "MX",
"sourceAmount": {
"currency": "USDC"
},
"destinationAmount": {
"amount": "200",
"currency": "MXN"
},
"blockchain": "ETH-SEPOLIA",
"senderType": "INDIVIDUAL",
"recipientType": "INDIVIDUAL",
"transactionVersion": "VERSION_2"
}
'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "2792f4a6-f1bd-4435-b681-1da309122159",
"paymentMethodType": "SPEI",
"blockchain": "ETH-SEPOLIA",
"senderCountry": "US",
"destinationCountry": "MX",
"createDate": "2025-09-24T00:01:13.532073875Z",
"quoteExpireDate": "2025-09-24T00:01:42.502094Z",
"cryptoFundsSettlementExpireDate": "2025-09-24T01:01:12.502097Z",
"sourceAmount": {
"amount": "15.000000",
"currency": "USDC"
},
"destinationAmount": {
"amount": "252.91",
"currency": "MXN"
},
"fiatSettlementTime": {
"min": "0",
"max": "5",
"unit": "MINUTES"
},
"exchangeRate": {
"rate": "16.860667",
"pair": "USDC/MXN"
},
"fees": {
"totalAmount": {
"amount": "1.568971",
"currency": "USDC"
},
"breakdown": [
{
"type": "TAX_FEE",
"amount": {
"amount": "0.234663",
"currency": "USDC"
}
},
{
"type": "BFI_TRANSACTION_FEE",
"amount": {
"amount": "0.138037",
"currency": "USDC"
}
},
{
"type": "CIRCLE_SERVICE_FEE",
"amount": {
"amount": "0.000000",
"currency": "USDC"
}
},
{
"type": "BLOCKCHAIN_GAS_FEE",
"amount": {
"amount": "1.196271",
"currency": "USDC"
}
}
]
},
"senderType": "INDIVIDUAL",
"recipientType": "INDIVIDUAL",
"certificate": {
// certificate object
},
"quoteOptions": {
"isFirstParty": false
},
"transactionVersion": "VERSION_2"
}
]
}
```
> **Note:** The quote returned from this step must follow the Transactions V2
> workflow, you can't switch from V2 back to V1 without first recreating the
> quote.
### Step 2. Create a V2 transaction
You would then follow the same payment API workflow as before, create a payment
from the [create a payment](/api-reference/cpn/cpn-platform/create-payment)
endpoint. After your payment is in the `CRYPTO_FUNDS_PENDING` state, initiate
the transaction using the
[create a transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v2/cpn/payments/:paymentId/transactions \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey" : "${RANDOM_UUID}"
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "dbc27d23-cd4f-447e-855e-349cb2853d23",
"status": "CREATED",
"paymentId": "49d4231e-6c4f-319e-946d-ed8c8bab5abc",
"expireDate": "2025-09-08T20:02:06.651391Z",
"blockchain": "ETH-SEPOLIA",
"senderAddress": "0x1234567890123456789012345678901234567890",
"destinationAddress": "0x0000000000000000000000000000000000000001",
"amount": {
"amount": "15.000000",
"currency": "USDC"
},
"messageType": "PAYMENT_SETTLEMENT_CONTRACT_V1_0_PAYMENT_INTENT",
"messageToBeSigned": {
"domain": {
"name": "Permit2",
"chainId": "11155111",
"verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
"message": {
"permitted": {
"token": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"amount": "14174474"
},
"spender": "0xe2B17D0C1736dc7C462ABc4233C91BDb9F27DD1d",
"nonce": "25668617285137697861288274946631174355105919960416755114569514179393151588120",
"deadline": "1757362866",
"witness": {
"from": "0x1234567890123456789012345678901234567890",
"to": "0x0000000000000000000000000000000000000001",
"value": 14174474,
"validAfter": "1757358106",
"validBefore": "1757361726",
"nonce": "0x38bfec2b230187932870d575132e8ae1f83b34c10e3bf6d64c377f0c13245718",
"beneficiary": "0x4f1c3a0359A7fAd8Fa8E9E872F7C06dAd97C91Fd",
"maxFee": "0",
"attester": "0x768919ef04853b5fd444ccff48cea154768a0291",
"requirePayeeSign": false
}
},
"primaryType": "PermitWitnessTransferFrom",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "PaymentIntent"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"PaymentIntent": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "validAfter",
"type": "uint256"
},
{
"name": "validBefore",
"type": "uint256"
},
{
"name": "nonce",
"type": "bytes32"
},
{
"name": "beneficiary",
"type": "address"
},
{
"name": "maxFee",
"type": "uint256"
},
{
"name": "requirePayeeSign",
"type": "bool"
},
{
"name": "attester",
"type": "address"
}
]
}
},
"metadata": {}
}
}
```
### Step 3. Sign and submit the transaction
From the response in the previous step, extract the `messageToBeSigned` field.
You must sign this data using [EIP-712](https://eips.ethereum.org/EIPS/eip-712)
typed data signing from your sender wallet. Once signed, you should submit it to
the
[submit transaction V2](/api-reference/cpn/cpn-platform/submit-transaction-v2)
endpoint. The following is an example request:
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v2/cpn/payments/:paymentId/transactions/:transactionId/submit \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-type: application/json' \
--data '
{
"signedTransaction": "0x12b5fb72e99f9bb0300d2eb66a6d89dd5a667f43669893cf14bfcc390754dcb61b69f92cba598ec83a184e11c97e3bb9964a2bfd7a09688eee63f586ad9ccae21c"
}
'
```
**Note:** For Solana, follow the steps in [How-to: Create an Onchain
Transaction](/cpn/guides/transactions/create-an-onchain-txn) to sign the
transaction. You would submit the signed transaction in the same way as for
EVM.
After the transaction is submitted, CPN is responsible for broadcasting the
transaction to the blockchain. You will be notified by webhooks when events
related to the transaction occur. Unlike Transaction V1, you don't need to
actively monitor the transaction or manually accelerate it. CPN monitors the
transaction and automatically accelerates it if necessary.
# How-to: Set up a Circle wallet for CPN payments
Source: https://developers.circle.com/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments
Configure a Circle-hosted dev-controlled wallet as your operational wallet for CPN onchain payment flows.
Use Circle-hosted [dev-controlled wallets](/wallets/dev-controlled.mdx) as your
operational wallet for CPN onchain payment flows. Circle custody keeps signing
keys secure and surfaces the wallet address and wallet ID through the Wallet
APIs.
Treasury is your responsibility: fund the wallet on the right blockchain,
reconcile balances, and size liquidity for your payment volume. When Circle also
moves your fiat and USDC, pair this guide with
[Set up Circle on/off-ramps for CPN payments](/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments).
If you custody keys yourself, see
[Bring your own wallet for CPN](/cpn/concepts/wallets/bring-your-own-wallet).
## Overview
* Use this guide when you want Circle to host the wallet that signs CPN onchain
transactions for USDC transfers.
* CPN does not require Circle-hosted wallets. The steps here are optional; they
support teams that want Circle custody for the sender wallet.
* After your wallet holds USDC on the required blockchain, you can continue with
[Integrate with CPN as an OFI](/cpn/quickstarts/integrate-with-cpn-ofi) or
[Create an onchain transaction](/cpn/guides/transactions/create-an-onchain-txn).
Dev-controlled Programmable Wallets are documented in the [Circle
Wallets](/wallets) section. This topic frames those capabilities for CPN
payments only.
## Prerequisites
Before you begin:
* You have a [CPN Console](https://cpn.circle.com/signin) account with the
[Circle Wallet](/wallets/dev-controlled) capability enabled for your
organization.
* You have an API key for Programmable Wallets. Create and manage keys in
[CPN Console → Developer → API Keys](https://cpn.circle.com/signin).
* You have sandbox or mainnet access enabled for Programmable Wallets.
Mainnet access for Programmable Wallets is typically granted after your CPN
eligibility application is approved, when **Circle Wallets** was requested as
a capability.
## Steps
This guide covers generating and registering your entity secret, creating a
wallet set and EOA wallet, configuring notifications, and funding the wallet
with USDC.
### Step 1. Generate and register your entity secret
The entity secret is a 32-byte value that secures your developer-controlled
wallets. Circle never stores the secret in plain text: you must protect it.
Register your entity secret:
Follow
[Register your entity secret](/wallets/dev-controlled/register-entity-secret)
for Node.js and Python: install the SDK, generate the entity secret, and
register the ciphertext with Circle. That guide includes tabs for each language.
Use environment variables or a secrets manager for `apiKey` and `entitySecret`;
never commit real values.
Generate a cryptographically random 32-byte value and store it in a secure
location (for example in a secrets manager). From a terminal you can use:
```bash theme={null}
openssl rand -hex 32
```
Encrypt the secret with Circle's public key, then register the ciphertext with
the Programmable Wallets configuration API. Read
[Entity secret management](/wallets/dev-controlled/entity-secret-management) for
encryption, ciphertext, and non-SDK options, including the sample repository
linked from that topic.
The HTTP API uses `POST /v1/w3s/config/entity/entitySecret` with a body
containing `entitySecretCiphertext`. Store the recovery file from the response
if you need to recover access later.
### Step 2. Create a wallet set and operational wallet
A wallet set groups wallets under one entity secret. Create a wallet set, then
create at least one externally owned account (EOA) wallet.
Use the API reference for request and response fields:
* [Create wallet set](/api-reference/wallets/developer-controlled-wallets/create-wallet-set)
(`POST /v1/w3s/developer/walletSets`)
* [Create wallet](/api-reference/wallets/developer-controlled-wallets/create-wallet)
(`POST /v1/w3s/developer/wallets`)
Example request body for a single sandbox EOA on Sepolia (adjust names and
ciphertext for your environment):
**1. Create wallet set** - example request body:
```json theme={null}
{
"name": "CPN Operational Wallets",
"entitySecretCiphertext": ""
}
```
**2. Create wallet** - example request body for the second call. Replace
`` with the `walletSetId` from the create-wallet-set
response:
```json theme={null}
{
"idempotencyKey": "",
"entitySecretCiphertext": "",
"walletSetId": "",
"blockchains": ["ETH-SEPOLIA"],
"count": 1,
"accountType": "EOA"
}
```
Save the **wallet ID** and **address** from the create-wallet response. CPN uses
the wallet ID for transaction APIs; funding flows use the address.
Use `ETH-SEPOLIA` for sandbox testing. On mainnet, set `blockchains` to the
production identifier that matches your CPN corridor (for example, `ETH` for
Ethereum mainnet). See
[Supported blockchains](/cpn/references/blockchains/supported-blockchains).
### Step 3. Configure webhook notifications for wallet activity
Subscribe to wallet and transaction events so your system is notified when funds
arrive or transfers complete.
1. Open [CPN Console → Developer → Webhooks](https://cpn.circle.com/signin) and
add or update your subscriber configuration as offered for your organization.
2. For Programmable Wallets subscription details and payload shapes, see
[Webhook notifications](/api-reference/webhooks),
[Create subscription](/api-reference/wallets/common/create-subscription)
(`POST /v2/notifications/subscriptions`), and the
[`transactions.inbound`](/api-reference/wallets/common/transactions-inbound)
and
[`transactions.outbound`](/api-reference/wallets/common/transactions-outbound)
webhook events.
### Step 4. Fund your wallet with USDC
Your operational wallet must hold USDC before you initiate a CPN payment that
draws from that wallet.
* **Sandbox:** Use the [Circle faucet](https://faucet.circle.com/) to obtain
testnet USDC. Enter your wallet address and select the matching testnet
blockchain.
* **Mainnet:** Fund the wallet by moving USDC from your Circle business account
balance (for example after an on-ramp). Follow how to
[Set Up Circle On/Off-Ramps for CPN Payments](/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments).
You can exercise the Circle APIs flow in sandbox before switching to
production endpoints.
Confirm funding with
[List wallet balance](/api-reference/wallets/developer-controlled-wallets/list-wallet-balance)
(`GET /v1/w3s/wallets/{id}/balances`). When your USDC balance is sufficient for
your test or production payment, you are ready to call CPN transaction APIs.
## See also
* [Integrate with CPN as an OFI](/cpn/quickstarts/integrate-with-cpn-ofi)
(continue at Part 1: Request a quote).
* [Set up Circle on/off-ramps for CPN payments](/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments)
(fund your operational wallet via Circle Mint).
* [Set up a webhook endpoint](/api-reference/webhook-endpoints) (CPN payment
events)
* [Webhook events](/cpn/references/webhooks/webhook-events)
* [Entity secret management](/wallets/dev-controlled/entity-secret-management)
# Managed Payments
Source: https://developers.circle.com/cpn/managed-payments
CPN Managed Payments is a turnkey stablecoin solution in which Circle takes on licensing, compliance, minting, and global settlement.
CPN Managed Payments gives partners stablecoin settlement without needing
in-house custody, compliance, or onchain systems. Circle holds assets, runs
compliance, and does the onchain work. You call APIs for subaccounts, funding,
withdrawals, and payins or payouts. You stay in fiat at the edge. Payment
platforms and banks can offer stablecoin payments under their brand without
custody or compliance builds in-house. See the
[Managed Payments API](#api-summary).
## Key features
* **Fiat funding and withdrawals:** Send or receive fiat over wire and other
methods Circle supports.
* **Stablecoin payins and payouts:** Send and receive USDC on
[supported blockchains](/cpn/references/blockchains/supported-blockchains).
Payins credit each subaccount; send payouts from each subaccount or the main
wallet when enabled.
* **Merchant subaccounts:** Give each merchant or customer a separate stablecoin
account for clear bookkeeping and audit trails. Use the Accounts API to list,
create, and fetch these accounts. Your agreement's onboarding model determines
whether you or Circle provisions them.
* **Webhook notifications:** Receive webhook events for payment intents,
payments, address book recipients, payouts, and credit transfers. See
[Webhook events](/cpn/managed-payments/references/webhook-events) for event
types and payloads.
* **Line of credit and reporting:** Borrow USDC on credit when your agreement
allows it. Daily and monthly payout and balance reports are available.
* **Two onboarding models:** Your Managed Payments agreement with Circle sets
which model you use.
* **Direct:** Circle works directly with your merchants/customers. You collect
Know Your Business (KYB) of your merchant and submit it through the
[End User Onboarding API](/cpn/managed-payments/end-user-onboarding). Circle
creates subaccounts for the merchants/customers after KYB approval.
* **Intermediary:** Circle doesn't have to onboard your underlying merchants
or customers. You create subaccounts for each merchant or customer with the
APIs.
## How it works
You join through Circle's invitation and sign up with a Managed Payments
account. Circle assigns role-based permissions to your API keys, controlling
access to specific capabilities such as subaccounts, wires, stablecoin payouts,
reports, and balance views. Circle configures these roles during onboarding.
Creating merchant subaccounts depends on the onboarding process you agree on
with Circle.
* **Direct onboarding:** You collect end-merchant Know Your Business (KYB)
information and submit it through the
[End User Onboarding API](/cpn/managed-payments/end-user-onboarding). Circle
creates subaccounts for those merchants on your behalf after KYB approval.
* **Intermediary model:** Use the
[accounts API](/api-reference/cpn/managed-payments/accounts/create-account)
to create the subaccounts you need for your merchants. Subaccounts keep
balances separate for tracking and reporting.
* **Payins:**
1. [Create a continuous payment intent](/api-reference/cpn/managed-payments/payment-intents/create-payment-intent)
for third-party payins so each subaccount has a stable USDC receive address.
2. After USDC is confirmed, use
[Fiat Burns](/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal)
to withdraw USDC to fiat in your bank account.
* **Payouts:**
1. Use the
[Wires API](/api-reference/cpn/managed-payments/wires/create-account-wire-account)
to add bank accounts and wire instructions, then send fiat so Circle credits
USDC.
2. Use the
[Payouts API](/api-reference/cpn/managed-payments/payouts/create-payout)
to send USDC onchain to payees you register and approve in the
[Address Book](/api-reference/cpn/managed-payments/address-book/create-address-book-recipient).
* **Line of credit (optional):** If your agreement supports it, you can borrow
USDC before fiat settles.
## API summary
| Area | Purpose |
| :-------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
| [Accounts API](/api-reference/cpn/managed-payments/accounts/list-accounts) | Create subaccounts for merchants. List and get stablecoin accounts for self and merchant subaccounts. |
| [Payins](/api-reference/cpn/managed-payments/payment-intents/create-payment-intent) | Payment intents and Payments for payin flows. |
| [Fiat Burns](/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal) | Withdraw USDC to fiat in your bank when enabled. |
| [Payouts](/api-reference/cpn/managed-payments/payouts/create-payout) | Onchain USDC sends plus funding, Address Book, and Line of Credit when enabled. |
| [Reporting](/api-reference/circle-mint/account/request-report) | Request daily and monthly payin, payout, and wallet balance reports. |
For more details, see the
[Managed Payments API reference](/api-reference/cpn/managed-payments/accounts/list-accounts).
## Get started
Pick a quickstart to walk through a managed payment flow:
Payment intents, inbound USDC, and settlement to fiat for your use case.
Subaccounts, funding, and USDC payouts for your managed payments use case.
# Settlement flows
Source: https://developers.circle.com/cpn/managed-payments/concepts/settlement-flows
How Managed Payments moves value between fiat and USDC for payin-heavy and payout-heavy setups
When you use Managed Payments, you settle in USDC while your books stay in fiat.
Circle holds assets in regulated custody, runs compliance checks, and moves
funds both onchain and to your bank. Two settlement flows support this model:
* [Payins](#payins-usdc-to-fiat) (USDC-to-fiat): Inbound USDC that Circle
converts and deposits as fiat in your bank account.
* [Payouts](#payouts-fiat-to-usdc) (fiat-to-USDC): Fiat you send to Circle that
backs outbound USDC to payees you designate.
You can run one flow, the other, or both depending on your volume and product
design. The following sections walk through how each flow works, when to choose
one over the other, and what APIs you call at each stage.
## Payins (USDC-to-fiat)
A payin settles inbound USDC as fiat in your bank account. Circle manages the
custodial accounts, performs compliance screening, and executes a first-party
burn to move value offchain.
### How a payin moves through the system
1. **Payer sends USDC onchain.** The USDC arrives in a Circle Managed Payments
account. Many integrations create a continuous payment intent on each
subaccount so every merchant or segment has a stable receive address.
2. **Circle screens and confirms the deposit.** Circle's compliance and policy
engine evaluates the transaction before it progresses.
3. **USDC is burned.** Circle performs a first-party burn to remove the USDC
from circulation and convert the value to fiat.
4. **Fiat is credited to your bank.** After the burn completes, Circle initiates
a withdrawal to your designated bank account for local disbursement.
### Key API calls for payins
| Action | Endpoint |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Create a payment intent | [Create payment intent](/api-reference/cpn/managed-payments/payment-intents/create-payment-intent) (Payment Intents API) |
| List payments | [List payments](/api-reference/cpn/managed-payments/payments/list-payments) (Payments API) |
| Burn USDC and withdraw fiat | [Create account withdrawal](/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal) (Fiat Burns API) |
### When to use payins
Choose the payin flow when your end users pay in USDC and you need to receive
fiat: for example, a merchant checkout where customers pay with USDC and you
disburse to sellers in local currency. To try it, follow the
[Receive Stablecoin Payins quickstart](/cpn/managed-payments/quickstarts/receive-stablecoin-payins).
## Payouts (fiat-to-USDC)
A payout converts fiat you send to Circle into USDC that payees receive onchain.
You fund your account by wire, Circle mints or allocates USDC, and you initiate
crypto payouts to registered addresses.
### How a payout moves through the system
1. **You wire fiat to Circle.** Wires can credit a specific subaccount or, if
your agreement allows it, a main wallet used for pooled funding. Set up bank
accounts and wire instructions ahead of time through the
[Wires API](/api-reference/cpn/managed-payments/wires/create-account-wire-account).
2. **Fiat posts and a deposit record is created.** After the wire settles, you
can list and retrieve deposit records through the
[Fiat Deposits API](/api-reference/cpn/managed-payments/deposits/list-account-deposits).
3. **Circle converts fiat to USDC.** USDC may remain in your account briefly
while payouts are prepared.
4. **You register approved payout addresses.** Circle's policy engine screens
each address before it is eligible to receive funds.
5. **USDC is sent to payees onchain.** Circle executes the crypto payout to the
approved addresses.
Subaccounts per merchant or segment give you built-in audit trails and simplify
reconciliation.
### Key API calls for payouts
| Action | Endpoint |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Set up wire instructions | [Create account wire account](/api-reference/cpn/managed-payments/wires/create-account-wire-account) (Wires API) |
| List fiat deposits | [List account deposits](/api-reference/cpn/managed-payments/deposits/list-account-deposits) (Fiat Deposits API) |
| Register payout address | [Create address book recipient](/api-reference/cpn/managed-payments/address-book/create-address-book-recipient) (Address Book API) |
| Send USDC payout | [Create payout](/api-reference/cpn/managed-payments/payouts/create-payout) (Payouts API) |
### Optional line of credit
Some agreements include a line of credit so USDC is available before every wire
clears. This lets you initiate payouts without waiting for each wire to settle.
Eligibility and terms depend on your agreement with Circle.
### When to use payouts
Choose the payout flow when you need to disburse USDC to external recipients:
for example, paying out creators, suppliers, or partners who prefer to receive
stablecoins. To try it, follow the
[Send Stablecoin Payouts quickstart](/cpn/managed-payments/quickstarts/send-stablecoin-payouts).
For details on how subaccounts and the main wallet interact during payouts, see
[Subaccount architecture](/cpn/managed-payments/concepts/sub-account-architecture).
## Comparing payins and payouts
| Dimension | Payins (USDC-to-fiat) | Payouts (fiat-to-USDC) |
| --------------------- | ------------------------------ | ------------------------------------ |
| **Direction** | USDC in, fiat out | Fiat in, USDC out |
| **Funding source** | Payer sends USDC onchain | You wire fiat to Circle |
| **Settlement output** | Fiat credited to your bank | USDC sent to payee addresses onchain |
| **Typical use case** | Merchant checkout, collections | Creator payouts, supplier payments |
| **Key APIs** | Fiat Burns | Wires, Fiat Deposits |
## Integration considerations
* Circle operates the regulated infrastructure for custody and money movement.
You integrate through the
[Managed Payments API](/api-reference/cpn/managed-payments/accounts/list-accounts).
* You can combine both flows in a single integration if your product requires
bidirectional movement.
* Use the payin, payout, and balance reports Circle provides for reconciliation.
* For a hands-on walkthrough, see
[Quickstart: Receive stablecoin payins](/cpn/managed-payments/quickstarts/receive-stablecoin-payins)
and
[Quickstart: Send stablecoin payouts](/cpn/managed-payments/quickstarts/send-stablecoin-payouts).
* For subaccount setup and balance tracking, see
[Subaccount architecture](/cpn/managed-payments/concepts/sub-account-architecture).
# Subaccount architecture
Source: https://developers.circle.com/cpn/managed-payments/concepts/sub-account-architecture
Subaccounts, your main wallet, and how they work together in Managed Payments
A Managed Payments subaccount is a separate ledger for a merchant or segment. It
belongs to your Managed Payments account for your setup.
* Most teams create one subaccount per merchant or segment. Then USDC balances
and activity stay easy to trace in your books and in Circle reports.
* Every setup also includes a main wallet. USDC can live in the main wallet
only, in subaccounts only, or across both, depending on how you fund and pay
out.
Together these accounts let you split balances for books and reports while still
using shared funding or pooled sends when your setup allows.
## Wallet structure and typical flows
The following figure is a simplified model of a Managed Payments integration
with subaccounts. Circle enables only the paths that your setup and agreement
support.
```mermaid theme={null}
flowchart TB
subgraph mp_setup["Managed Payments setup"]
MW[Main wallet]
SA[Subaccount A]
SB[Subaccount B]
end
Wire[Fiat wire] --> MW
Wire --> SA
LOC[Line of credit] -.-> MW
MW -.-> SA
SA --> Payin[Payment intent receive]
SB --> Payin
SA --> Payout[USDC payout]
SB --> Payout
MW --> Payout
```
Solid arrows are typical funding, receive, and payout paths, including payouts
funded from the main wallet. Dotted arrows are optional paths your setup may
use, such as a line of credit into the main wallet or moving USDC to a
subaccount before payout.
## Accounts API
If you use the direct onboarding model, Circle handles the creation of
subaccounts for you. Submit end-merchant KYB through the [Partner Onboarding
API reference](/api-reference/circle-mint/onboarding/list-applications), and
Circle creates subaccounts on your behalf after approval.
Use the Digital Asset Accounts API to create and inspect the stablecoin account
that backs each merchant subaccount.
* [Create a managed payments intermediary account](/api-reference/cpn/managed-payments/accounts/create-account)
when you add a merchant subaccount in the intermediary onboarding model.
* [List accounts](/api-reference/cpn/managed-payments/accounts/list-accounts) to
page through subaccounts and see each one's balances and metadata.
* [Get an account](/api-reference/cpn/managed-payments/accounts/get-account) to
load one subaccount's stablecoin account by `accountId`, including balances
and metadata.
Each subaccount has both an `accountId` and a `walletId`. Both IDs point to the
same subaccount. The `accountId` names the stablecoin account record. The
`walletId` names the wallet that holds the account's USDC balance. API requests
and responses use one or both, depending on the endpoint.
Circle grants role-based API access when you onboard. The endpoints your API
keys can call depend on the roles set for your account.
In the intermediary model, you call the
[create account](/api-reference/cpn/managed-payments/accounts/create-account)
endpoint after each merchant is approved. In the direct model, Circle provisions
subaccounts during onboarding. In both cases, you reference the same `accountId`
values for wires, payouts, and payins.
## Subaccount lifecycle
Use a subaccount's `status` to determine whether it can process activity:
* `pending`: Circle is creating the subaccount, but it isn't operational yet.
* `active`: The subaccount can process supported activity.
* `rejected`: Circle rejected the subaccount creation request, and the
subaccount can't be used.
* `archived`: The subaccount is retired and excluded from account listings by
default. Read requests still work, but most requests that change the account
are blocked.
To retire an active subaccount, first reduce all of its balances to zero. Then
[archive the account](/api-reference/cpn/managed-payments/accounts/archive-account).
The request is idempotent, so you can safely retry it. To retrieve archived
subaccounts, list accounts with the `status=archived` query parameter.
Archiving doesn't permanently close a subaccount. An incoming deposit to an
existing deposit address or wire instruction automatically returns the
subaccount to `active` status. After you archive a subaccount, stop sending
funds to its existing deposit details if you intend to keep it retired.
## Funding and pooled balances
Depending on your setup, USDC can be funded and spent across the main wallet and
subaccounts in several ways:
* Wires can credit one subaccount so that merchant's USDC sits in its own
balance. Use the
[Wires API](/api-reference/cpn/managed-payments/wires/create-account-wire-account)
for bank accounts and wire instructions.
* If your setup allows it, you can mint to a shared main wallet instead. That
pool can back steady, high-volume payout use cases.
* Some setups include a line of credit. USDC may land in the main wallet first;
payouts can move funds to the correct subaccount before they go onchain. Terms
follow your agreement with Circle.
* Payouts usually debit a subaccount balance. If your integration supports it,
you can fund payouts from the main wallet instead of from each subaccount.
That helps when you send a lot and don't want USDC in every subaccount first.
Use that path only when your setup supports it.
## Payins and payouts
Handle payins, debits, and reconciliation per subaccount:
* **Payins (receiving USDC):** Each subaccount can have a stable onchain
address. Create a continuous payment intent so that each merchant or segment
has a dedicated receive address.
* **Payouts and withdrawals:** Specify the `walletId` or `accountId` of the
subaccount to debit when you create a payout.
The two money-out endpoints expect different `source.type` tokens for the same
subaccount: [create a
payout](/api-reference/cpn/managed-payments/payouts/create-payout) (`POST
/v1/payouts`) requires `source.type` = `wallet` with the `walletId`, while
[create a
withdrawal](/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal)
(`POST /v1/accounts/withdrawals`) requires `source.type` = `account` with the
`accountId`.
| Activity | API / resource | Key identifier |
| ------------------ | ------------------------- | ----------------------------------- |
| Receive USDC | Continuous payment intent | `paymentIntentId`, onchain address |
| Send USDC | Payouts API | `walletId` or `accountId` |
| Match transactions | Settlement reports | `accountId`, transaction timestamps |
# End User Onboarding
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding
Programmatically onboard your end users to Managed Payments using the End User Onboarding API
The End User Onboarding API lets you create and manage onboarding applications
for your customers. Collect business data, upload documents, and submit
applications for compliance review through one REST API. This removes manual
steps as you scale.
Use the End User Onboarding API to onboard your end users (the merchants and
customers you serve) to [Managed Payments](/cpn/managed-payments). Circle
performs Know Your Business (KYB) verification on each end user, and the End
User Onboarding API submits that KYB data programmatically, so you onboard end
users through the API instead of offline channels. The API covers the full
application lifecycle, from draft through approval, including Requests for
Information (RFIs) when the compliance team needs more data.
Managed Payments serves two customer types: you (the distributor) and your end
users. You access Managed Payments and obtain API keys through the [CPN
Console](https://console.circle.com/). Your end users onboard through the End
User Onboarding API, which uses the same API structure for both customer
types.
## Key features
Retrieve a JSON Schema that defines required fields, validation rules, and
conditional dependencies.
Submit all sections in one call. The API validates first, then saves each in
order.
Upload, list, and delete documents. Files link to the relevant application
fields and entities.
## What you can build
Build onboarding forms in your platform. Use the JSON Schema to render fields
and conditional sections. Your customers stay in your app the whole time.
Migrate existing customers by creating applications and submitting data in bulk.
One request per application fills all sections at once.
Surface RFIs to your operations team or to customers. Webhook notifications
drive real-time updates and automate follow-ups.
## How it works
You create a draft application for your customer. It starts in `DRAFT`
status with a schema that lists the required fields.
You save section data and upload documents. The API validates each
submission against the schema.
When all sections are complete, you submit the application. Circle's
compliance team reviews it and either approves it or issues requests for
information (RFIs).
If the team requests more data, you update the sections or documents and
resubmit. The application moves through statuses until it reaches a final
state.
## Get started
Walk through an application end to end in sandbox
Learn the statuses an application moves through
Walk through the full application lifecycle
Respond to compliance team RFIs and resubmit
# Array sections
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/concepts/array-sections
Understand how to work with repeatable entity sections like beneficial owners and directors
Some sections represent repeatable entities such as `beneficial_owners`,
`authorized_signers`, or `directors`. These are **array sections**. The schema
defines them with `type: array` containing `items` that describe each entity.
Array sections differ from flat sections in how data is read, saved, and linked
to documents.
[Conditional logic](/cpn/managed-payments/end-user-onboarding/concepts/conditional-logic)
can activate or deactivate array sections based on the application's
configuration.
## How array sections differ from flat sections
| Operation | Flat section | Array section |
| --------- | -------------------------- | --------------------------------------------------------------------------------- |
| Read | Returns a JSON object | Returns a JSON array; each element includes a `refId` (UUID) |
| Save | `PUT` with a JSON object | `PUT` with a JSON array; include `refId` for existing entities, omit for new ones |
| Delete | Not applicable | `DELETE /sections/{sectionName}/{refId}` removes a single entity |
| Documents | Link with `datumName` only | Link with `datumName` and the entity's `refId` |
## Reading array data
Retrieve array section data with the
[Get section](/api-reference/end-user-onboarding/get-section) endpoint
(`GET /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}`).
The response is a JSON array where each element includes a `refId`:
```json theme={null}
{
"data": [
{
"refId": "550e8400-e29b-41d4-a716-446655440001",
"firstName": "Jane",
"lastName": "Doe",
"ownershipPercentage": 51
},
{
"refId": "660e8400-e29b-41d4-a716-446655440002",
"firstName": "John",
"lastName": "Smith",
"ownershipPercentage": 49
}
]
}
```
## Saving array data
When saving, include `refId` for entities you want to update and omit it for new
entities:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/beneficial_owners \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '[
{
"refId": "550e8400-e29b-41d4-a716-446655440001",
"firstName": "Jane",
"lastName": "Doe",
"ownershipPercentage": 60
},
{
"firstName": "Alex",
"lastName": "Johnson",
"ownershipPercentage": 40
}
]'
```
In this example, the first entity is updated (existing `refId`) and the second
is created (no `refId`). Existing entities not included in the array are
preserved, not deleted.
## Removing entities
Remove a single entity from an array section with the
[Remove entity](/api-reference/end-user-onboarding/remove-entity) endpoint
(`DELETE /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}/{refId}`).
The response includes the updated section list with recalculated statuses.
## Document association
When uploading a document for a field in an array entity, include the entity's
`refId` so the document is associated with the correct person or organization.
See
[Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
for the full upload workflow.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=passport_document' \
--form 'refId=550e8400-e29b-41d4-a716-446655440001' \
--form 'issuedCountry=US'
```
Omitting `refId` when the document belongs to an array section entity causes the
upload to fail with a `400` error.
# Conditional logic
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/concepts/conditional-logic
Understand how Nature of Business and JSON Schema conditionals control which fields are required
The End User Onboarding schema uses JSON Schema `if/then` constructs to express
conditional requirements. Not all sections and fields apply to every
application. The schema defines what is required based on the application's
configuration.
## Nature of business
The primary conditional driver is **Nature of Business (NOB)**. This
classification is set during application creation and determines which sections
and fields appear. For example, a `cryptoExchange` application may require
different documentation than a `trust` application.
## How conditionals work
The `allOf` array in the schema contains `if/then` blocks:
* The `if` clause checks the value of a parent field (most commonly
`nature_of_business`).
* When the condition is met, the `then` clause adds fields to the `required`
array, making them mandatory.
Supported condition types:
| Condition | Description |
| ------------------ | ---------------------------------------- |
| Exact string match | A field equals a specific value |
| boolean match | A field is `true` or `false` |
| `enum` (OR) | A field matches any value in a list |
| `contains` | An array field contains a specific value |
| Negation | A field does not match a value |
The following example shows an exact string match condition:
```json theme={null}
{
"if": {
"properties": {
"natureOfBusiness": { "const": "cryptoExchange" }
}
},
"then": {
"properties": {
"cryptoExchangeLicenseNumber": {
"type": "string",
"minLength": 1
}
},
"required": ["cryptoExchangeLicenseNumber"]
}
}
```
In this example, when `natureOfBusiness` is `cryptoExchange`, the schema
requires the `cryptoExchangeLicenseNumber` field. The same `if`/`then` pattern
applies to all condition types in the preceding table. Substitute the
appropriate JSON Schema keyword (`enum` for OR lists, `contains` for array
membership, `not` for negation) in the `if` block.
## Conditional side effects
After
[saving a section](/cpn/managed-payments/end-user-onboarding/howtos/create-and-populate-applications#step-4-save-section-data)
that contains a conditional parent field (for example, `beneficialOwnersExist`),
other sections may activate or deactivate. This includes
[array sections](/cpn/managed-payments/end-user-onboarding/concepts/array-sections),
which can be conditionally activated. The API signals this through two
mechanisms:
1. **`sectionsChanged` flag**: Returned in every save response. When `true`, the
section list has changed and you should re-fetch sections.
2. **Updated `sections` array**: The response includes the current status of all
active sections, not just the one you saved.
Always check the `sectionsChanged` flag after saving section data. If you ignore
it, your integration may submit incomplete applications or miss newly required
sections.
## Mutually exclusive groups
Some conditionals create mutually exclusive branches. For example, different
variants of `basic_business_info` may exist for different NOB values. Only one
branch's fields are required at a time. The schema enforces this automatically.
## Design considerations
* Integrations that skip re-fetching after a `sectionsChanged` response risk
submitting incomplete applications.
* Client-side validation against the schema (`minLength`, `pattern`, `enum`)
before submitting reduces round-trips and improves the user experience.
* Use the `?resolved=true` query parameter on the schema endpoint to retrieve a
pruned view containing only the fields that are currently active based on
previously submitted data. This removes the need to evaluate conditional rules
locally.
# How-to: Create and populate applications
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/howtos/create-and-populate-applications
Retrieve onboarding applications, discover the schema, and save section data
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Populate an onboarding application by creating a client, reading the schema,
listing sections, and saving field data (individually or in bulk), and cancel
draft applications you no longer need.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[CPN Console](https://console.circle.com).
## Steps
### Step 1. Create a client and initialize the application
#### 1.1. Create a client record and initialize onboarding
Create a client record and initialize an onboarding application in one request:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/partner/clients \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"clientName": "Acme Trust Co.",
"country": "US",
"clientType": "business",
"businessDetails": {
"natureOfBusiness": "trust",
"institutionType": "trust"
}
}'
```
For the full list of request fields, see the
[Create partner client](/api-reference/end-user-onboarding/create-partner-client)
API reference.
**Example response:**
```json theme={null}
{
"data": {
"clientEntityId": "880e8400-e29b-41d4-a716-446655440099",
"applicationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
#### 1.2. Save the application ID
Save the `applicationId`. You'll use it in all subsequent
`/v1/onboarding/partner/applications/...` calls.
Using the same `clientName` and `country` combination returns a `409 Conflict`
error. Use a different `clientName` to create another client in the same
country.
#### 1.3. Retrieve the newly created application
Retrieve the newly created application to inspect its type, sections, and
status:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"type": "ONBOARDING",
"status": "DRAFT",
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "not_started" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
],
"pendingRfis": []
}
}
```
The `type` field identifies the purpose of the application. Partners can create
only `ONBOARDING` applications through the client creation flow shown here.
Other application types may appear in application responses, but they are not
partner-created through this flow. See
[Application types](/cpn/managed-payments/end-user-onboarding/references/application-states#application-types)
for the full list of types.
### Step 2. Discover the schema
Retrieve the JSON Schema (draft 2020-12) for your application. It defines every
required field:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/schema \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The schema is a single JSON document. Top-level properties map to section names.
Each section lists its fields, types, and validation rules such as `minLength`,
`maxLength`, `pattern`, `enum`, and `format`.
Use this schema to:
* Enumerate all sections and their fields.
* Drive client-side form rendering and validation.
* Understand which fields are conditional. See
[Conditional logic](/cpn/managed-payments/end-user-onboarding/concepts/conditional-logic)
for details.
Pass `?resolved=true` to receive a schema pruned against the application's
current saved data. Conditional branches that have not been activated are
removed, so you only see the fields relevant to the current application state.
### Step 3. List all sections
List all sections with their current completion status:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
Each section has a status: `not_started`, `incomplete`, `complete`, or
`action_required`.
**Example response:**
```json theme={null}
{
"data": {
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "not_started" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
]
}
}
```
To include each section's current field data in the same response, add
`?includeData=true`.
### Step 4. Save section data
Save data for a single section:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/basicBusinessInfo \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
}'
```
The server validates the payload against the JSON Schema before saving. If
validation fails, the API returns a `422` response with per-field errors. Each
error includes a dot-notation path, error code, and message.
**Example `422` error response:**
```json theme={null}
{
"errors": [
{
"path": "businessWebsite",
"code": "format",
"message": "must be a valid URL"
}
]
}
```
Saving a section may change other sections' statuses. For example, a conditional
section may become required. The response includes a `sectionsChanged` flag.
Check this flag and re-fetch sections as needed.
The response also includes a `fieldsChanged` flag. When `true`, the server
modified one or more field values during save (for example, clearing a field
that is no longer applicable after a conditional change). Re-fetch the section
data if you need to display the current saved state.
**Example response:**
```json theme={null}
{
"data": {
"data": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
],
"sectionsChanged": false,
"fieldsChanged": false
}
}
```
### Step 5. Bulk save (optional)
If your backend already has all the data, use the bulk save endpoint. It fills
multiple sections in one request:
```shell theme={null}
curl --request PATCH \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/data \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"basicBusinessInfo": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"physicalBusinessAddress": {
"physicalAddressLine1": "123 Main St",
"physicalAddressCity": "San Francisco",
"physicalAddressCountry": "US",
"physicalAddressPostalCode": "94102"
}
}'
```
Validation is atomic. The API checks all sections against the JSON Schema before
saving. If any section fails, no data is written. The response lists errors
across all sections.
After validation passes, sections save one at a time. Saving a section may
activate other conditional sections. If a failure occurs mid-save, earlier
sections may already be persisted.
#### When to use bulk vs. per-section
| Use case | Recommended endpoint |
| -------------------------------------------------- | ----------------------------- |
| Interactive form-based UI (one section at a time) | `PUT /sections/{sectionName}` |
| Programmatic integration with all data available | `PATCH /data` |
| RFI response updating multiple affected sections | `PATCH /data` |
| Reading saved data or deleting individual entities | `GET`/`DELETE /sections/...` |
### Step 6. Read all saved data
To retrieve the complete current state of an application in one request, use the
bulk read endpoint:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/data \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The response `data` field is an object keyed by section name, the same structure
as the `PATCH /data` request body, making it straightforward to pre-fill a UI or
sync application state to your backend. The `sections` array lists each visible
section with its current status. For full parameter and response details, see
the [Get all application data](/api-reference/end-user-onboarding/get-all-data)
API reference.
**Example response:**
```json theme={null}
{
"data": {
"data": {
"basicBusinessInfo": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"physicalBusinessAddress": {
"physicalAddressLine1": "123 Main St",
"physicalAddressCity": "San Francisco",
"physicalAddressCountry": "US",
"physicalAddressPostalCode": "94102"
}
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "complete" }
]
}
}
```
## Cancel an application
Cancel a `DRAFT` application that is no longer needed:
```shell theme={null}
curl --request DELETE \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
You can only cancel applications in `DRAFT` status. Other statuses return a
`409` error.
## See also
* [Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
* [Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
* [Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states#application-types)
* [Conditional logic](/cpn/managed-payments/end-user-onboarding/concepts/conditional-logic)
* [Array sections](/cpn/managed-payments/end-user-onboarding/concepts/array-sections)
# How-to: Handle requests for information
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis
Respond to compliance team RFIs by updating data, re-uploading documents, and resubmitting applications
Resolve compliance team requests so your application can proceed to approval.
List RFI bundles to see what's needed, update the requested fields or documents,
respond with comments, and resubmit the application.
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[CPN Console](https://console.circle.com).
* Confirmed your application is in `PENDING_CUSTOMER_INFORMATION` status. See
[Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
for the full lifecycle.
## Steps
### Step 1. List RFI bundles
When an application moves to `PENDING_CUSTOMER_INFORMATION`, the `pendingRfis`
array on the application response contains the bundle IDs. List all bundles:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"bundles": [
{
"bundleId": "660e8400-e29b-41d4-a716-446655440010",
"status": "PUBLISHED",
"createdAt": "2026-03-26T10:00:00Z",
"rfis": [
{
"rfiId": "770e8400-e29b-41d4-a716-446655440011",
"section": "beneficial_owners",
"field": "passport_document",
"type": "COLLECT_DOCUMENT",
"status": "PENDING",
"systemComment": "Document is illegible. Please upload a clearer copy.",
"createdAt": "2026-03-26T10:00:00Z"
}
]
}
]
}
}
```
Each RFI identifies the `section` and `field` that needs attention. The
`systemComment` explains what the compliance team requires.
### Step 2. View RFI detail and comments
Retrieve the full detail for a specific RFI, including any prior comments:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
### Step 3. Update the requested data
For RFIs with type `UPDATE_FIELD` or `NEW_FIELD`, submit the corrected field
data directly to the RFI:
```shell theme={null}
curl --request PATCH \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com"
}'
```
Replace the key with the `datumName` from the RFI detail response. The API
validates the value against the application schema and marks the RFI as
responded.
For RFIs with type `COLLECT_DOCUMENT`, re-upload the document instead. See
[Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
for the upload workflow.
### Step 4. Respond with a comment
Add a comment to an RFI to communicate with the compliance team:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID}/comments \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"content": "Updated the field as requested."
}'
```
### Step 5. Resubmit the application
After you address all open RFIs, resubmit the application so it moves back to
`SUBMITTED` for another round of review:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true
}'
```
This is the same endpoint used during initial submission. For details on
optional fields such as `certificationIds`, see
[Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications#step-2-submit-the-application).
## See also
* [Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
* [Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
* [Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
# How-to: Submit and track applications
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications
Submit applications for review and handle Requests for Information
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Submit a completed application for compliance review and track its progress
through approval. After submission, monitor status changes through webhooks or
polling, and list applications by status as needed.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[CPN Console](https://console.circle.com).
* Completed all application sections (each has `complete` status).
* Uploaded all required documents. See
[Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents).
## Steps
### Step 1. Discover required certifications (GUI flows only)
Some applications require your end user to actively review and agree to one or
more compliance certifications before you submit on their behalf. Whether you
need to perform this step depends on how your integration is built:
* **API-only integrations:** If you control the submission flow programmatically
and your end users never see the terms directly (for example, you have a
separate agreement process in your own product), you can skip this step and
omit `certificationIds` from the submit request.
* **GUI / end-user flows:** If you are building a UI where end users read and
explicitly agree to Circle's certification terms, you must fetch the active
certifications for the application, present them to the user, and pass the
agreed certification IDs in the submit request.
To retrieve the active certifications for an application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/certifications \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": [
{
"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"content": "I certify that ..."
}
]
}
```
Display each certification's `content` (HTML) to your end user and collect their
acknowledgment. Then pass every `id` in the `certificationIds` array when you
submit (see step 2 below). If the set of IDs you provide does not exactly match
the application's active certifications, the API returns a `422` error with
details about which IDs are missing or unknown.
### Step 2. Submit the application
Submit an application when all sections have `complete` status:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true,
"certificationIds": [
"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
]
}'
```
For the full request schema, see the
[Submit application](/api-reference/end-user-onboarding/submit-application) API
reference.
The application moves from `DRAFT` to `SUBMITTED`. If any sections are
incomplete, the server returns a `409` error.
**Example response:**
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"type": "ONBOARDING",
"status": "SUBMITTED"
}
}
```
### Step 3. Track application status
Monitor your application's progress by polling, or use webhook notifications for
real-time updates (see [Webhook endpoints](/api-reference/webhook-endpoints) to
subscribe):
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
See
[Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
for a full list of statuses and transitions, and
[Webhook notifications](/cpn/managed-payments/end-user-onboarding/references/webhook-notifications)
for the event types and payloads delivered to your subscription.
### Step 4. List all applications
List applications for your partner account. You can filter by status:
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/onboarding/partner/applications?status=PENDING_CUSTOMER_INFORMATION&limit=25' \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The response supports pagination. Use `starting_after` with the `applicationId`
of the last item to fetch the next page.
If the application moves to `PENDING_CUSTOMER_INFORMATION`, the compliance team
has issued one or more Requests for Information (RFIs). See
[Handle requests for information](/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis)
for the full RFI workflow.
## See also
* [Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
* [Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
* [Handle requests for information](/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis)
# How-to: Upload documents
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/howtos/upload-documents
Upload, list, and manage supporting documents for onboarding applications
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Attach supporting files to an onboarding application (such as identity
documents, certificates, or proof of address), then list, download, or delete
them while the application is still in a mutable state.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[CPN Console](https://console.circle.com).
* Confirmed your application is in `DRAFT` or `PENDING_CUSTOMER_INFORMATION`
status.
* Identified which `datumName` fields need files (from the schema).
## Steps
### Step 1. Upload a document
Upload a file and link it to a field in the application:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=authorizedRepGovernmentId' \
--form 'issuedCountry=US' \
--form 'documentType=passport'
```
#### Required fields
| Field | Description |
| ------------- | -------------------------------------------------------------------------------- |
| `fileContent` | Binary file content |
| `fileName` | File name with extension (for example, `passport.pdf`) |
| `datumName` | Schema field name this file satisfies (for example, `authorizedRepGovernmentId`) |
#### Conditionally required fields
| Field | Description |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `refId` | UUID of the entity this file belongs to. Required for [array sections](/cpn/managed-payments/end-user-onboarding/concepts/array-sections). |
| `issuedCountry` | ISO 3166-1 alpha-2 country code. Required for identity documents. |
| `documentType` | Form of identity document being uploaded. Required when the schema field includes an `x-documentTypes` extension. Standard government ID fields accept `passport`, `usdl` (US driver's license), or `usid` (US state ID). Notarized government ID fields accept `idNotarization`. |
To find which `documentType` values are accepted for a given field, retrieve the
application schema
(`GET /v1/onboarding/partner/applications/{applicationId}/schema`) and inspect
the `x-documentTypes` array on the target field. Fields without
`x-documentTypes` do not require this parameter.
**Example response:**
```json theme={null}
{
"data": {
"documentId": "550e8400-e29b-41d4-a716-446655440002"
}
}
```
Uploading to a submitted application returns a `409` error. The application must
be in `DRAFT` or `PENDING_CUSTOMER_INFORMATION`.
### Step 2. List all documents
List all files for an application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"documents": [
{
"documentId": "550e8400-e29b-41d4-a716-446655440002",
"fileName": "passport.pdf",
"datumName": "passport_document",
"uploadedAt": "2026-03-25T14:30:00Z"
}
]
}
}
```
### Step 3. Download a document
Download a file you uploaded:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents/${DOCUMENT_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--output document.pdf
```
The response body is binary data with a `Content-Type: application/octet-stream`
header.
### Step 4. Delete a document
Delete a document that is no longer needed:
```shell theme={null}
curl --request DELETE \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents/${DOCUMENT_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
A successful deletion returns `204 No Content`.
You can only delete documents while the application is in `DRAFT` or
`PENDING_CUSTOMER_INFORMATION` status. Deleting a document from a submitted
application returns a `409` error.
## See also
* [Create and populate applications](/cpn/managed-payments/end-user-onboarding/howtos/create-and-populate-applications)
* [Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
* [Array sections](/cpn/managed-payments/end-user-onboarding/concepts/array-sections)
(for `refId` usage)
# Submit onboarding application
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/quickstarts/submit-onboarding-application
Submit an onboarding application using the End User Onboarding API in sandbox
Walk through the full application lifecycle: create a client, populate its
sections, upload a document, and submit for review.
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[CPN Console](https://console.circle.com) (**Developer > API Keys**). Use this
key to obtain a Bearer token through the Circle key exchange.
* (Recommended) Set up a webhook subscription for application status changes to
receive real-time updates instead of polling. See
[Webhook endpoints](/api-reference/webhook-endpoints).
## Step 1: Create a client and initialize the application
Create a client and start an onboarding application in one request:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/partner/clients \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"clientName": "Acme Trust Co.",
"country": "US",
"clientType": "business",
"businessDetails": {
"natureOfBusiness": "trust",
"institutionType": "trust"
}
}'
```
For the full request schema, see
[Create Partner Client](/api-reference/end-user-onboarding/create-partner-client).
**Example response:**
```json theme={null}
{
"data": {
"clientEntityId": "880e8400-e29b-41d4-a716-446655440099",
"applicationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
Save the `applicationId`. You'll use it in all subsequent steps.
Using the same `clientName` and `country` combination returns a `409 Conflict`
error. Use a different `clientName` to create another client in the same
country.
## Step 2: Discover the schema
The sections and fields in an application aren't fixed. They're defined by a
JSON Schema (draft 2020-12) specific to the business type you set with
`natureOfBusiness` and `institutionType`. Read the schema before you save data
so you know which sections and fields apply. Don't assume field names.
Retrieve the schema for your application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/schema \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
Top-level properties map to section names, and each section lists its fields,
types, and validation rules. To list sections with their completion status
instead, call
`GET /v1/onboarding/partner/applications/${APPLICATION_ID}/sections`.
For schema discovery, conditional fields, and bulk save, see
[Create and populate applications](/cpn/managed-payments/end-user-onboarding/howtos/create-and-populate-applications).
## Step 3: Save section data
Using the field names from the schema, populate a section. This example saves
the `basicBusinessInfo` section for the trust application created in Step 1. The
sections and fields for your application depend on its own schema:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/basicBusinessInfo \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
}'
```
Section and field names vary by business type. Saving a field that isn't in
the section's schema returns a `422` error with per-field details. Use the
schema from Step 2 to see the fields for your application.
The response confirms the section status changed to `complete`:
```json theme={null}
{
"data": {
"data": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" }
],
"sectionsChanged": false
}
}
```
Repeat for each remaining section. See
[Create and populate applications](/cpn/managed-payments/end-user-onboarding/howtos/create-and-populate-applications)
for details on the schema, bulk save, and conditional sections.
## Step 4: Upload a document
Upload a supporting document and link it to a schema field:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=passport_document' \
--form 'issuedCountry=US'
```
The response returns the new document ID:
```json theme={null}
{
"data": {
"documentId": "550e8400-e29b-41d4-a716-446655440002"
}
}
```
See
[Upload documents](/cpn/managed-payments/end-user-onboarding/howtos/upload-documents)
for details on required fields and linking documents to array entities.
## Step 5: Submit the application
After all sections are `complete` and required documents are uploaded, submit
the application:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true
}'
```
If your integration presents compliance certifications directly to end users
(for example, in a GUI where users read and agree to terms), fetch the active
certifications with `GET /v1/onboarding/partner/applications/${APPLICATION_ID}
/certifications` before submitting and include the agreed certification UUIDs
in a `certificationIds` array in the request body. See [Submit and track
applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
for details.
The application moves to `SUBMITTED`:
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"status": "SUBMITTED"
}
}
```
## Step 6: Check application status
Poll for the current status or use webhook notifications (if you set up a
subscription in the prerequisites) for real-time updates:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The application progresses through `IN_REVIEW` and reaches a terminal status of
`APPROVED` or `DENIED`. If the compliance team needs more information, the
status changes to `PENDING_CUSTOMER_INFORMATION`.
For more details, see:
* [Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states):
the full lifecycle of an application
* [Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications):
the submission workflow
* [Handle requests for information](/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis):
responding when the compliance team needs more data
# Application types and states
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/references/application-states
Reference for all application types and their statuses
Application types and states describe why an application exists, where it is in
the review process, and which actions are available.
## Application types
The application `type` field identifies the purpose of an application.
| Type | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ONBOARDING` | Initial application used to onboard a client. Partners can create this application type. |
| `INFORMATION_UPDATE` | Application used to update customer information and send RFIs. Partners cannot create this application type. |
| `PERIODIC_REVIEW` | Application used to require customers to review and resubmit information. Partners cannot create this application type. |
## Application statuses
| Status | Description |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `DRAFT` | Created but not yet submitted. Data can be added, updated, or deleted. |
| `SUBMITTED` | Submitted for review. No modifications allowed. |
| `IN_REVIEW` | Compliance review in progress (automated and manual checks). |
| `PENDING_CUSTOMER_INFORMATION` | One or more RFIs issued. Partner must update data or documents and resubmit. |
| `APPROVED` | Approved. Terminal state. |
| `DENIED` | Denied. Terminal state. |
| `CANCELLED` | Canceled. Terminal state. |
## Status transitions
See [Application types](#application-types) for definitions of `ONBOARDING`,
`PERIODIC_REVIEW`, and `INFORMATION_UPDATE`.
### `ONBOARDING` applications
```mermaid theme={null}
flowchart LR
DRAFT --> SUBMITTED
DRAFT --> CANCELLED
SUBMITTED --> IN_REVIEW
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
IN_REVIEW --> APPROVED
IN_REVIEW --> DENIED
```
### `PERIODIC_REVIEW` applications
The system creates periodic review applications in `IN_REVIEW`, then moves them
to `DRAFT` when partner action is required.
```mermaid theme={null}
flowchart LR
IN_REVIEW --> DRAFT
DRAFT --> SUBMITTED
DRAFT --> CANCELLED
SUBMITTED --> IN_REVIEW
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
IN_REVIEW --> APPROVED
IN_REVIEW --> DENIED
```
### `INFORMATION_UPDATE` applications
The system creates information update applications in `IN_REVIEW`. They move to
`PENDING_CUSTOMER_INFORMATION` only when partner action is required.
```mermaid theme={null}
flowchart LR
IN_REVIEW --> APPROVED
IN_REVIEW --> CANCELLED
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
SUBMITTED --> IN_REVIEW
```
See
[Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
for the submission and RFI workflow.
## Mutable vs. immutable states
Partners can only modify application data (sections, documents) in these states:
* **`DRAFT`**: Initial state after creation. Full read/write access.
* **`PENDING_CUSTOMER_INFORMATION`**: RFIs issued. Partners can update the
sections and documents referenced by the RFIs, then resubmit.
All other states are immutable. API calls that attempt to modify data return a
`409 Conflict` error.
Saving section data in mutable states may trigger conditional changes. See
[Conditional logic](/cpn/managed-payments/end-user-onboarding/concepts/conditional-logic)
for details.
## Section statuses
Each section in an application has its own status that tracks data completeness:
| Status | Description |
| ----------------- | ----------------------------------------------------- |
| `not_started` | No data saved for this section |
| `incomplete` | Some data saved but required fields are missing |
| `complete` | All required fields are populated and valid |
| `action_required` | An RFI targets a field in this section; update needed |
Terminal states (`APPROVED`, `DENIED`, `CANCELLED`) are final. Applications
cannot transition out of these states.
# Webhook notifications
Source: https://developers.circle.com/cpn/managed-payments/end-user-onboarding/references/webhook-notifications
Reference for End User Onboarding webhook event types with example payloads.
End User Onboarding delivers asynchronous application lifecycle events as
webhook notifications so you can drive your integration with real-time updates
instead of polling
[`GET /v1/onboarding/partner/applications/{id}`](/api-reference/end-user-onboarding/get-application).
Notifications are delivered as Amazon SNS messages on the
[v1 notification system](/api-reference/webhooks#notification-api-versions). To
register a webhook endpoint, create a subscription for End User Onboarding
notifications in the [CPN Console](https://console.circle.com) under
**Developer > Subscriptions**, then see
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
## When notifications fire
Webhooks are sent only for **asynchronous** outcomes that you cannot observe
from the response to your own API call. Synchronous transitions you trigger
directly—such as `DRAFT` → `SUBMITTED` from a submit request—are not delivered
as webhooks because the result is already in the HTTP response.
| Notification type | Fires when the application transitions to | From |
| -------------------------------------- | ----------------------------------------- | ---------------------- |
| `onboarding_application_rfi_requested` | `PENDING_CUSTOMER_INFORMATION` | `IN_REVIEW` |
| `onboarding_application_approved` | `APPROVED` | `IN_REVIEW` |
| `onboarding_application_denied` | `DENIED` | `IN_REVIEW` |
| `onboarding_application_cancelled` | `CANCELLED` | `DRAFT` or `IN_REVIEW` |
| `onboarding_periodic_review_requested` | `DRAFT` | `IN_REVIEW` |
`onboarding_application_rfi_requested` signals that the compliance team has
issued one or more Requests for Information (RFIs). See
[Handle requests for information](/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis)
for the RFI workflow, and
[Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
for the full lifecycle.
## Payload
Every End User Onboarding notification carries the same flat payload.
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------ |
| `clientId` | UUID | Identifier of the partner the application belongs to. Used to route the notification to your endpoint. |
| `notificationType` | string | The event type, for example `onboarding_application_rfi_requested`. |
| `version` | number | Notification system version. Always `1`. |
| `applicationId` | UUID | Identifier of the affected onboarding application. |
| `status` | string | The application's new status. |
| `previousStatus` | string | The application's status immediately before this transition. |
`status` and `previousStatus` use the same values as
[Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states).
Internal review sub-states are collapsed to `IN_REVIEW`, so a transition out of
review reports `previousStatus` as `IN_REVIEW`.
## Event types
### `onboarding_application_rfi_requested`
The compliance team issued one or more RFIs and the application moved to
`PENDING_CUSTOMER_INFORMATION`. Fetch the open RFIs with
[List RFIs](/api-reference/end-user-onboarding/list-rfis) (the application's
`pendingRfis` array also carries the bundle IDs), respond to each, then
resubmit.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_rfi_requested",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "PENDING_CUSTOMER_INFORMATION",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_approved`
Compliance review completed and the application was approved. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_approved",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "APPROVED",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_denied`
Compliance review completed and the application was denied. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_denied",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "DENIED",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_cancelled`
The application was canceled. `previousStatus` reflects the application's status
before cancellation and depends on the application type. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_cancelled",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "CANCELLED",
"previousStatus": "DRAFT"
}
```
### `onboarding_periodic_review_requested`
A periodic review application moved from `IN_REVIEW` back to `DRAFT`, making it
editable. Use this notification to start the partner response flow for the
review request.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_periodic_review_requested",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "DRAFT",
"previousStatus": "IN_REVIEW"
}
```
## See also
* [Application types and states](/cpn/managed-payments/end-user-onboarding/references/application-states)
* [Submit and track applications](/cpn/managed-payments/end-user-onboarding/howtos/submit-and-track-applications)
* [Handle requests for information](/cpn/managed-payments/end-user-onboarding/howtos/handle-rfis)
# Quickstart: Receive stablecoin payins
Source: https://developers.circle.com/cpn/managed-payments/quickstarts/receive-stablecoin-payins
Managed Payments Stablecoin payins using the default continuous payment intent flow; for transient payment intents, see the Crypto Deposits quickstart
This guide walks you through the basics of accepting USDC on your site or in
your app with Managed Payments stablecoin payins. You create payment intents,
show a deposit address, your customer sends USDC, and Circle links the onchain
payment to your intent. The steps use a continuous payment intent, which is the
default pattern and fits a stable receive address per merchant or subaccount.
The alternative [Quickstart: Crypto
Deposits](/circle-mint/crypto-payments-quickstart) walkthrough covers a
transient payment intent instead (amount and time window set on create). Use
that guide when you want a one-time, expiring checkout-style intent.
## Prerequisites
Before you start, ensure you have:
* Aligned with Circle on your Managed Payments onboarding model (Direct or
Intermediary).
* Completed Managed Payments onboarding, depending on your preferred model, with
Circle and have API credentials for the environment you target. The cURL
examples use the sandbox host `api-sandbox.circle.com`.
* Roles that allow creating and reading payment intents and payments in your
setup. Circle assigns roles during onboarding.
* Subaccounts or any other structure your setup requires so each continuous
intent ties to the right merchant or segment. See
[Managed Payments](/cpn/managed-payments) and the
[Digital Asset Accounts API](/api-reference/cpn/managed-payments/accounts/create-account)
if you create accounts first.
* (Optional) A webhook receiver or queue to handle payment intent and payment
events. See
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
* (Optional) For an end-to-end test, a payer wallet with USDC and native gas on
the chain you set on the intent (for example Sepolia ETH when the intent uses
an Ethereum test chain).
## Sequence diagram
Continuous payins use settlement currency and chain on create; you may see
`active` instead of `pending` when the address is ready.
```mermaid theme={null}
sequenceDiagram
participant Customer
participant Site as Your website
participant Server as Your server
participant CAPI as Circle APIs
Customer->>Site: Select pay with stablecoin onchain
Site->>Server: Create deposit intent settlement and chain
Server->>CAPI: POST v1 paymentIntents
CAPI-->>Server: Payment intent created
CAPI-->>Server: Webhook payment intent address ready
Server-->>CAPI: Ack
Server->>Site: Deposit address
Site->>Customer: Present deposit address
Customer->>Customer: Send payment from wallet to deposit address
CAPI-->>Server: Webhook payment paid
Server-->>CAPI: Ack
Server->>Site: Stablecoin payment completed
Site->>Customer: Payment confirmed
```
## Steps
1. Set up payment intent to pay with stablecoin
2. Acquire blockchain address customer will pay to
3. Customer pays
4. Receive payment
## 1. Pay with stablecoin
Once the customer reaches checkout and confirms they would like to pay with USDC
on Ethereum, your system sends Circle a request to
[create a payment intent](/api-reference/cpn/managed-payments/payment-intents/create-payment-intent).
In this **continuous** payment intent you specify the settlement currency and
chain (no fixed charge amount on create).
Set both `currency` and `settlementCurrency`. Circle LLC (United States) and
Circle Singapore (`CIRCLE_SG`) also require `purposeOfTransfer`. Use a
[payment reason code](/cpn/references/errors/payment-reason-codes) other than
`PMT006`.
Create a payment intent:
```curl cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"currency": "USD",
"settlementCurrency": "USD",
"purposeOfTransfer": "PMT001",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
]
}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import {
Circle,
CircleEnvironments,
PaymentIntentCreationRequest,
} from "@circle-fin/circle-sdk";
import crypto from "crypto";
const circle = new Circle(
"",
CircleEnvironments.sandbox, // API base url
);
async function createContinuousPaymentIntent(): Promise {
const reqBody: PaymentIntentCreationRequest = {
currency: "USD",
settlementCurrency: "USD",
purposeOfTransfer: "PMT001",
paymentMethods: [
{
type: "blockchain",
chain: "ETH",
},
],
idempotencyKey: crypto.randomUUID(),
};
const resp = await circle.cryptoPaymentIntents.createPaymentIntent(reqBody);
console.log(resp.data);
}
void createContinuousPaymentIntent();
```
```json Response theme={null}
{
"data": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
],
"timeline": [
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:41:19.979669Z",
"merchantWalletId": "1000700366",
"currency": "USD",
"purposeOfTransfer": "PMT001"
}
}
```
```json Webhook notification theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"paymentIntent": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
],
"timeline": [
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:41:19.979669Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
## 2. Acquire blockchain address customer will pay to
For security reasons, the API does not return the deposit blockchain address in
the create response. To retrieve the blockchain deposit address, you have two
options:
1. Subscribe to webhook notifications
2. Poll Circle APIs
### Option 1: Webhook notification
To receive webhook notifications, follow the steps in
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
After you subscribe to notifications, you receive updates for the payment intent
whenever the resource is updated. In this case, when the
`paymentMethods.address` is set, a notification is sent with a new `timeline`
object with a `status` of `active`.
```json Payment intent webhook notification theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"paymentIntent": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
}
],
"timeline": [
{
"status": "active",
"time": "2026-03-23T17:41:23.450386Z"
},
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:41:23.405690Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
### Option 2: Poll payment intent endpoint
If you prefer to poll
[get a payment intent](/api-reference/cpn/managed-payments/payment-intents/get-payment-intent),
send `GET` requests until you receive `paymentMethods.address`.
Retrieve payment intent:
```curl cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/{id}' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle(
"",
CircleEnvironments.sandbox, // API base url
);
function delay(time: number): Promise {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
async function getPaymentIntent(paymentIntentId: string) {
const resp =
await circle.cryptoPaymentIntents.getPaymentIntent(paymentIntentId);
return resp.data;
}
async function pollPaymentIntent(): Promise {
const paymentIntentId = "payment-intent-id";
const pollInterval = 500; // Interval (in ms) by which to poll
let resp: Awaited>;
while (true) {
resp = await getPaymentIntent(paymentIntentId);
const depositAddress = resp.data?.paymentMethods[0]?.address;
if (depositAddress) break;
await delay(pollInterval);
}
console.log(resp);
}
void pollPaymentIntent();
```
```json Response theme={null}
{
"data": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
}
],
"timeline": [
{
"status": "active",
"time": "2026-03-23T17:41:23.450386Z"
},
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:41:23.405690Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
## 3. Enable customer payment
Once you receive the deposit address from `paymentMethods.address`, share it
with the customer so they can send funds on the correct chain.
A **continuous** payment intent doesn't define a fixed amount on create; you
communicate any specific charge (for example an invoice total) from your own
checkout or UI.
You can present the address two ways:
1. as plain text the customer can cut and paste; or
2. as a QR code the customer can scan using an app.
The customer then sends payment from their wallet (custodial or non-custodial).
When using transient payment intents, you can set a time frame in which the
customer must send payment by adjusting the `expiresOn` setting. The value
must be no more than 24 hours after creation or the API returns error `1128`.
## 4. Receive payment from Circle
Once Circle obtains payment onchain, Circle creates a Payment resource linked to
the Payment Intent created earlier and updates the status of that Payment
Intent. Your firm will then receive payment through the method specified.
### Option 1: Webhook notifications
```json Payment intent webhook notification theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"paymentIntent": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "5.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
},
{
"type": "totalPaymentFees",
"amount": "0.00",
"currency": "USD"
}
],
"timeline": [
{
"status": "active",
"time": "2026-03-23T17:41:23.450386Z"
},
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:52:18.080633Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
```json Payment webhook notification theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "payments",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"payment": {
"id": "021ff661-e7d5-332f-bb9c-e43870608f26",
"type": "payment",
"status": "pending",
"amount": {
"amount": "5.00",
"currency": "USD"
},
"createDate": "2026-03-23T17:49:54.197Z",
"updateDate": "2026-03-23T17:49:54.259Z",
"merchantId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"merchantWalletId": "12345",
"paymentIntentId": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x6dbe810e3314546009bd6e1b29f9031211cda5d2"]
},
"depositAddress": {
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
},
"transactionHash": "0xfbc0f1c8256af3453fd3be7a1491e3581e072022a29ffc78cf129a662182305e"
}
}
```
### Option 2: Retrieve payment intent and payment
Retrieve a payment intent:
```curl cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/{id}' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle(
"",
CircleEnvironments.sandbox, // API base url
);
async function getPaymentIntent(): Promise {
const paymentIntentId = "";
const resp =
await circle.cryptoPaymentIntents.getPaymentIntent(paymentIntentId);
console.log(resp.data);
}
void getPaymentIntent();
```
```json Response theme={null}
{
"data": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "5.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
},
{
"type": "totalPaymentFees",
"amount": "0.00",
"currency": "USD"
}
],
"timeline": [
{
"status": "active",
"time": "2026-03-23T17:41:23.450386Z"
},
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:52:18.080633Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
Retrieve a payment:
```curl cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/payments/{id}' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle(
"",
CircleEnvironments.sandbox, // API base url
);
async function getPayment(): Promise {
const paymentId = "";
const resp = await circle.payments.getPayment(paymentId);
console.log(resp.data);
}
void getPayment();
```
```json Response theme={null}
{
"data": {
"id": "021ff661-e7d5-332f-bb9c-e43870608f26",
"type": "payment",
"status": "paid",
"amount": {
"amount": "5.00",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"createDate": "2026-03-23T17:49:54.197184Z",
"updateDate": "2026-03-23T17:52:17.910610Z",
"merchantId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"merchantWalletId": "12345",
"paymentIntentId": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"settlementAmount": {
"amount": "5.00",
"currency": "USD"
},
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x6dbe810e3314546009bd6e1b29f9031211cda5d2"]
},
"depositAddress": {
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
},
"transactionHash": "0xfbc0f1c8256af3453fd3be7a1491e3581e072022a29ffc78cf129a662182305e"
}
}
```
For blockchains that require a 'memo' or 'address tag' (XLM, HBAR, etc.), the
optional `addressTag` field will be present in the `depositAddress` object.
# Quickstart: Send stablecoin payouts
Source: https://developers.circle.com/cpn/managed-payments/quickstarts/send-stablecoin-payouts
Send USDC payouts with Managed Payments using the address book and optional main-wallet funding
This guide shows how to send a stablecoin payout with Managed Payments. You add
a blockchain recipient to your address book. You wait until the recipient is
active. You create a payout from a subaccount or the main wallet. You confirm
that the payout completes.
## Prerequisites
Before you start, ensure you have:
* Managed Payments onboarding done and API keys for the environment you use.
This guide uses the sandbox host `api-sandbox.circle.com`.
* Roles for address book and stablecoin payouts for your setup. Circle sets
roles when you onboard.
* A subaccount wallet ID for `source` when you don't pay from the main wallet
alone. Read [Managed Payments](/cpn/managed-payments) and
[Subaccount architecture](/cpn/managed-payments/concepts/sub-account-architecture).
* (Optional) A webhook endpoint for address book and payout events. See
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
## Sequence diagram
```mermaid theme={null}
sequenceDiagram
participant Server as Your server
participant CAPI as Circle APIs
Server->>CAPI: POST v1/addressBook/recipients
CAPI-->>Server: Recipient pending or inactive
CAPI-->>Server: Webhook or poll recipient active
Server->>CAPI: POST v1/payouts
CAPI-->>Server: Payout pending
CAPI-->>Server: Webhook or poll payout complete
```
## Steps
1. Create an address book recipient
2. Wait until the recipient is active
3. Create a payout
4. Confirm the payout completed
## 1. Create an address book recipient
Outbound sends use destinations in your address book. Create a recipient with
[create an address book recipient](/api-reference/cpn/managed-payments/address-book/create-address-book-recipient).
Circle Singapore (`CIRCLE_SG`) entities must include beneficiary `identity`
and `ownership` on create. For a hosted (custodial) beneficiary, add:
```json theme={null}
{
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
```
Obtain `vaspId` from
[list address book VASPs](/api-reference/circle-mint/payouts/list-address-book-vasps)
(`GET /v1/addressBook/vasps`). For a self-hosted beneficiary, set
`ownership.custody.type` to `self_hosted` (omit `vaspId`) and provide the nested
PII object matching `identity.type` for Travel Rule compliance. See
[create an address book recipient](/api-reference/cpn/managed-payments/address-book/create-address-book-recipient)
for the full SG schema.
Circle LLC (United States) entities register a recipient with just `chain` and
`address`; the VASP directory and `vaspId` don't apply and aren't available.
Don't create a payout until the recipient is **active**. The `status` you get
when you create a recipient depends on your setup. For example, **delayed
withdrawals** can keep a new entry **inactive** until it activates (see the
following note).
**Delayed withdrawals** are off by default for Managed Payments. You can't
turn them on or off yourself; contact Circle customer support if your setup
needs a different configuration. When delayed withdrawals are on, new address
book entries start inactive. Wait until they're **active** before you pay.
```curl cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/addressBook/recipients' \
--header 'Accept: application/json' \
--header 'X-Request-Id: fb7980ad-fd01-468b-98ff-2d9ecff67f86' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "ETH",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"email": "satoshi@circle.com",
"bns": "testbns",
"nickname": "test nickname desc"
}
}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import {
AddressBookRecipientRequest,
Chain,
Circle,
CircleEnvironments,
} from "@circle-fin/circle-sdk";
import crypto from "crypto";
const circle = new Circle("", CircleEnvironments.sandbox);
async function createAddressBookRecipient(): Promise {
const reqBody: AddressBookRecipientRequest = {
idempotencyKey: crypto.randomUUID(),
chain: Chain.Eth,
address: "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
metadata: {
email: "satoshi@circle.com",
bns: "testbns",
nickname: "test nickname desc",
},
};
const resp =
await circle.cryptoAddressBook.createAddressBookRecipient(reqBody);
console.log(resp.data);
}
void createAddressBookRecipient();
```
```json Response theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "pending",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
## 2. Wait until the recipient is active
You cannot pay an inactive recipient. Use webhooks or polling until `status` is
`active`.
### Option 1: Webhook notification
After you subscribe, you get updates when address book recipients change. This
sample shows an `active` recipient.
```json Address book recipient webhook notification theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"addressBookRecipient": {
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "active",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
### Option 2: Poll the recipient
Call
[get an address book recipient](/api-reference/cpn/managed-payments/address-book/get-address-book-recipient)
until `data.status` is `active`.
```curl cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/addressBook/recipients/dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe' \
--header 'Accept: application/json' \
--header 'X-Request-Id: 55990729-c59f-4cda-9edd-838cefaa1e42' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle("", CircleEnvironments.sandbox);
function delay(ms: number): Promise {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function pollRecipientActive(recipientId: string): Promise {
const pollIntervalMs = 500;
while (true) {
const resp =
await circle.cryptoAddressBook.getAddressBookRecipient(recipientId);
const status = resp.data?.data?.status;
if (status === "active") {
console.log(resp.data);
return;
}
await delay(pollIntervalMs);
}
}
void pollRecipientActive("dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe");
```
```json Response theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "active",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
## 3. Create a payout
Create the payout with
[create a payout](/api-reference/cpn/managed-payments/payouts/create-payout).
Set `destination` to the address book entry. Set `amount` and optional
`toAmount`. Set `source` when you use a subaccount.
If you omit `source`, the API withdraws from the **main wallet**.
**Receiving currency and Travel Rule:** If you omit `toAmount.currency`, the API
uses `amount.currency` as the receiving currency.
The `source.identities` object is required for payouts greater than or equal to
\$3,000. The object describes the **originator** (your organization), not the
recipient, for Financial Crimes Enforcement Network (FinCEN)
[Travel Rule](/circle-mint/howtos/transfer-on-chain#travel-rule-compliance)
compliance.
Enforcement depends on account configuration. `purposeOfTransfer` is also
required on Circle Singapore (`CIRCLE_SG`), Circle SAS (`CIRCLE_FR`), and some
Circle LLC (United States) payouts. Use a `PMT` code from `PMT000` to `PMT030`
except `PMT006`. See
[payment reason codes](/cpn/references/errors/payment-reason-codes).
**Main wallet funding:** For Managed Payments, `source.useMainWalletFunding`
defaults to `true`. When it is `true`, the API tops up from the **main wallet**
if the source wallet lacks funds. Set `source.useMainWalletFunding` to `false`
to require the full amount on the source wallet only.
**Insufficient funds:** If a payout fails because funds are not ready yet,
confirm your wire completed and USDC is available for the source wallet. See the
[Wires API](/api-reference/cpn/managed-payments/wires/create-account-wire-account)
and [Settlement flows](/cpn/managed-payments/concepts/settlement-flows).
The example uses a subaccount as `source`, sets `useMainWalletFunding` on
`source`, and uses an amount that needs `identities` for Travel Rule coverage.
Replace placeholder IDs with your own values.
```curl cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/payouts' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ff422eab-52fa-4a6e-bf07-b6b522786468' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345",
"useMainWalletFunding": true,
"identities": [
{
"type": "individual",
"name": "Satoshi Nakamoto",
"addresses": [
{
"line1": "100 Money Street",
"line2": "Suite 1",
"city": "Boston",
"district": "MA",
"postalCode": "01234",
"country": "US"
}
]
}
]
},
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
},
"purposeOfTransfer": "PMT001"
}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import {
Circle,
CircleEnvironments,
CryptoPayoutCreationRequest,
CryptoPayoutDestinationType,
IdentityTypeEnum,
MoneyCurrencyEnum,
ToAmountCurrencyEnum,
TransferSourceWalletLocationTypeEnum,
} from "@circle-fin/circle-sdk";
import crypto from "crypto";
/** Managed Payments: `useMainWalletFunding` on `source` (SDK types may lag the API). */
type PayoutSourceWallet = NonNullable & {
useMainWalletFunding?: boolean;
};
const circle = new Circle("", CircleEnvironments.sandbox);
async function createManagedPaymentsPayout(): Promise {
const sourceWallet: PayoutSourceWallet = {
type: TransferSourceWalletLocationTypeEnum.Wallet,
id: "12345",
useMainWalletFunding: true,
identities: [
{
type: IdentityTypeEnum.Individual,
name: "Satoshi Nakamoto",
addresses: [
{
line1: "100 Money Street",
line2: "Suite 1",
city: "Boston",
district: "MA",
postalCode: "01234",
country: "US",
},
],
},
],
};
const reqBody: CryptoPayoutCreationRequest = {
idempotencyKey: crypto.randomUUID(),
source: sourceWallet,
destination: {
type: CryptoPayoutDestinationType.AddressBook,
id: "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe",
},
amount: {
amount: "3000.14",
currency: MoneyCurrencyEnum.Usd,
},
toAmount: {
currency: ToAmountCurrencyEnum.Usd,
},
purposeOfTransfer: "PMT001",
};
const resp = await circle.payouts.createPayout(reqBody);
console.log(resp.data);
}
void createManagedPaymentsPayout();
```
```json Response theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
},
"status": "pending",
"updateDate": "2020-04-10T02:13:30.000Z",
"createDate": "2020-04-10T02:13:30.000Z"
}
}
```
## 4. Confirm the payout completed
Use webhooks or polling with
[get a payout](/api-reference/cpn/managed-payments/payouts/get-payout) to
confirm the payout finished onchain.
### Option 1: Webhook notification
```json Payout webhook notification theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payout",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2020-04-10T02:13:30.000Z",
"updateDate": "2020-04-10T02:13:30.000Z"
}
}
```
### Option 2: Poll the payout
```curl cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29' \
--header 'Accept: application/json' \
--header 'X-Request-Id: d36f3c00-9c98-4610-bfea-83995379995e' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```typescript TypeScript theme={null}
/**
* See installation instructions at
* https://developers.circle.com/circle-mint/circle-sdks
*/
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle("", CircleEnvironments.sandbox);
function delay(ms: number): Promise {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function pollPayoutComplete(payoutId: string): Promise {
const pollIntervalMs = 500;
while (true) {
const resp = await circle.payouts.getPayout(payoutId);
const status = resp.data?.data?.status;
if (status === "complete") {
console.log(resp.data);
return;
}
await delay(pollIntervalMs);
}
}
void pollPayoutComplete("b8627ae8-732b-4d25-b947-1df8f4007a29");
```
```json Response theme={null}
{
"data": {
"id": "2f3bca9a-2d0e-4aef-a511-026eefd3cc6f",
"destination": {
"type": "address_book",
"id": "eaca84eb-69fb-53d3-9dac-f69cb5f1541a"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"amount": "500.00",
"currency": "USD"
},
"externalRef": "0x41f8f2cd555e247716f3e9bc97366ac04a848fd2fde76732c894c6843fc6f8db",
"createDate": "2023-08-14T20:30:19.145913Z",
"updateDate": "2023-08-14T22:05:55.406292Z",
"sourceWalletId": "1000594146",
"fees": {
"amount": "1.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.01",
"currency": "USD"
},
"status": "complete"
}
}
```
# Error codes
Source: https://developers.circle.com/cpn/managed-payments/references/error-codes
HTTP status codes and numeric error codes returned by Managed Payments endpoints, with causes and remediation steps
Errors from Managed Payments endpoints use a flat response and the following
endpoint-specific numeric codes. For general errors returned by any Circle API,
see [API errors](/api-reference/errors).
## Error response shape
Managed Payments endpoints return errors as a flat object:
```json theme={null}
{
"code": 1177,
"message": "The purposeOfTransfer field is missing or invalid",
"errors": []
}
```
## Common error codes
| Status code | Error code | Description |
| ----------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `500` | `-1` | Circle couldn't process the request. Retry later. If it fails again, contact Circle support with the `errId`. |
| `401` | `1` | The `Authorization` header is missing or malformed. Pass the API key as a `Bearer` token. |
| `400` | `2` | A request field is missing or has the wrong type. Check `message` for the field name. |
| `403` | `3` | The API key can't use this resource, or your entity doesn't support it. |
| `401` | `4` | The API key is invalid, expired, or inactive. Check the key. |
| `429` | `5` | Too many requests. Wait, then retry. |
## Create a payment intent
Codes returned by `POST /v1/paymentIntents`.
| Status code | Error code | Description |
| ----------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `1127` | Send one blockchain payment method. The value can't be `null` or contain more than one method. |
| `400` | `1128` | The `expiresOn` value is missing, malformed, in the past, or more than 24 hours after creation. Set it to a valid timestamp no more than 24 hours from now. |
| `400` | `1130` | The currency and blockchain pair isn't supported. Use a supported pair or confirm support with Circle. |
| `400` | `1131` | The intent and settlement currency pair isn't supported. Use a supported pair. |
| `400` | `1132` | The amount is `0`, less than `0`, or has too many decimal places. Send a valid amount. |
| `400` | `1177` | `purposeOfTransfer` is missing or invalid. Enforcement depends on account configuration; Circle Singapore (`CIRCLE_SG`) and certain Circle LLC (`CIRCLE_INC`) accounts require it. See [payment reason codes](/cpn/references/errors/payment-reason-codes). |
| `400` | `1069` | The merchant doesn't own the `merchantWalletId`, or the wallet wasn't found. Check the ID. |
| `409` | `1083` | The idempotency key is bound to another request. Fetch that result, or use a new key for a new request. |
## Refund a payment intent
Codes returned by `POST /v1/paymentIntents/{paymentIntentId}/refund`.
| Status code | Error code | Description |
| ----------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `400` | `1130` | The refund currency and blockchain pair isn't supported. Use a supported pair. |
| `400` | `1137` | The intent has no completed crypto payment, or the intent failed. It can't be refunded. |
| `400` | `1138` | Use the source currency from the first payment or settlement. |
| `400` | `1141` | The refund currency must match the first payment. |
| `400` | `1142` | The amount is more than the balance that can be refunded. Reduce it. |
| `400` | `1145` | The 30-day refund window has passed. Handle the refund outside this API. |
| `400` | `1146` | A crypto payment is still pending. Wait for it to finish, then retry. |
| `400` | `1165` | Set one destination: a raw `address` and `chain`, or an `addressBookId`. |
| `400` | `1166` | Use an address book entry owned by the merchant's legal entity. Circle Singapore (`CIRCLE_SG`) requires this. |
| `400` | `1167` | The address book entry isn't active. Wait for approval, then retry. |
| `400` | `1168` | The crypto payment can't be refunded in its current state. Check the state before you retry. |
| `400` | `1169` | The total refund is more than the crypto payment. Reduce the amount. |
| `400` | `1170` | The payment doesn't belong to this intent. Use the correct payment. |
| `400` | `1178` | The refund amount must be greater than `0`. |
| `404` | `1126` | The payment intent wasn't found. Check `paymentIntentId`. |
| `404` | `1164` | The address book entry wasn't found. Check `addressBookId`. |
Circle Singapore (`CIRCLE_SG`) doesn't support a raw refund destination. Use
an `addressBookId` or set `useOriginalSender: true`.
## Address book recipients
Codes returned when creating or updating an address book recipient.
| Status code | Error code | Description |
| ----------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `2024` | Circle Singapore (`CIRCLE_SG`) recipients require `identity`. |
| `400` | `2025` | Circle Singapore (`CIRCLE_SG`) recipients require `ownership`. |
| `400` | `2026` | A hosted-custody recipient requires `ownership.custody.vaspId`. |
| `400` | `2027` | `ownership.type` isn't supported. Circle Singapore (`CIRCLE_SG`) recipients must use `third_party`. |
| `400` | `2028` | `ownership.custody.type` isn't supported for this entity. |
| `400` | `2029` | Set `ownership.custody`. |
| `400` | `2030` | `ownership.custody.vaspId` isn't a known VASP. |
| `400` | `2031` | `identity.type` must be `individual` or `business`. |
| `400` | `2032` | An individual recipient requires a first name in `identity.individual`. |
| `400` | `2033` | An individual recipient requires a last name in `identity.individual`. |
| `400` | `2034` | A business recipient requires a business name in `identity.business`. |
| `400` | `2035` | A self-hosted recipient must not set `ownership.custody.vaspId`. |
| `400` | `2038` | A self-hosted Circle Singapore (`CIRCLE_SG`) recipient requires `metadata.email`. Its `ownership.custody.type` is `self_hosted`. |
| `400` | `2048` | A self-hosted individual recipient requires `identity.individual`. |
| `400` | `2049` | A self-hosted business recipient requires `identity.business`. |
## Stablecoin payouts
Codes returned by stablecoin payout endpoints.
| Status code | Error code | Description |
| ----------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `5000` | The travel rule `identity.type` must be `individual` or `business`. |
| `400` | `5002` | The payout amount must be greater than `0`. |
| `400` | `5003` | The destination address is inactive. |
| `400` | `5004` | The destination address wasn't found. |
| `400` | `5005` | The source wallet wasn't found. |
| `400` | `5006` | The source wallet lacks funds. Check that its wire has settled. |
| `400` | `5007` | The currency isn't supported for this operation. |
| `400` | `5011` | The destination address is invalid. If you just added it, wait up to 24 hours before using it. |
| `400` | `5015` | The destination blockchain is invalid for the currency. |
| `400` | `5020` | `purposeOfTransfer` is missing or invalid on a payout. Enforcement depends on account configuration; Circle Singapore (`CIRCLE_SG`), Circle SAS (`CIRCLE_FR`), and certain Circle LLC (`CIRCLE_INC`) accounts require it. See [payment reason codes](/cpn/references/errors/payment-reason-codes). |
| `400` | `5021` | The destination address isn't associated with the source sub-wallet. |
## Create a withdrawal
Codes returned when `POST /v1/accounts/withdrawals` sends fiat to a linked bank
account.
| Status code | Error code | Description |
| ----------- | ---------- | --------------------------------------------------------------------------------- |
| `400` | `5002` | The payout amount must be greater than `0`. |
| `400` | `5006` | The source wallet lacks funds. Add funds or wait for settlement, then retry. |
| `400` | `5007` | The currency isn't supported. Use a supported currency. |
| `400` | `5016` | The fiat account can't use this transfer type. Check what the account supports. |
| `400` | `5017` | The transfer limit was reached. Reduce the amount or wait for the limit to reset. |
| `400` | `5019` | The payout limit was reached. Reduce the amount or wait for the limit to reset. |
| `400` | `1112` | The fiat account wasn't found. Check that it is registered. |
| `400` | `1100` | The fiat account type is invalid for payouts. |
| `400` | `1089` | The source account wasn't found. Verify the source account ID. |
## Asynchronous withdrawal outcomes
A valid `POST /v1/accounts/withdrawals` call returns `201` at once with
`data.status: "pending"`. Get the final result from the `payouts` webhook or
`GET /v1/accounts/withdrawals/{id}`. Store `trackingRef` to match the wire on
your bank statement.
| `data.status` | `data.errorCode` | Description |
| ------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `pending` | (omitted) | The withdrawal was accepted and is processing. Wait for the webhook. |
| `complete` | (omitted) | USD was sent to the bank. Record `trackingRef` for reconciliation. |
| `failed` | `insufficient_funds` | The account ran out of funds after acceptance. Retry once funds are replenished. |
| `failed` | `transaction_denied` | A policy or risk check denied the wire. Contact Circle support. Don't retry on your own. |
| `failed` | `transaction_failed` | Circle or the bank couldn't process the wire. Retry once. If it fails again, contact support with the withdrawal ID. |
| `failed` | `transaction_returned` | The bank sent the wire back. Fix the bank details, register the account again, and retry. |
| `failed` | `bank_transaction_error` | The bank reported an error. Contact your bank and Circle support. |
| `failed` | `fiat_account_limit_exceeded` | The bank account reached its inbound limit. Ask the bank to raise it, then retry. |
| `failed` | `invalid_bank_account_number` | The bank account number is wrong. Register the account again with the right number. |
| `failed` | `invalid_ach_rtn` | The ACH routing number is invalid. Circle Singapore (`CIRCLE_SG`) accounts use wires only. Use the right SWIFT/BIC. |
| `failed` | `invalid_wire_rtn` | The SWIFT/BIC is invalid. Register the account again with the right value. |
| `failed` | `vendor_inactive` | A bank used by Circle is down for now. Retry in 5 to 10 minutes. Contact support if it still fails. |
| `failed` | `payout_canceled` | The withdrawal was canceled before it settled. Submit a new withdrawal if you still need it. |
| `failed` | `invalid_travel_rule_identity` | The travel rule identity failed validation. Circle Singapore (`CIRCLE_SG`) requires it. Correct the details and retry. |
| `failed` | `intermediary_bank_required` | The wire needs an intermediary bank. Add intermediary bank details to the account, then retry. |
## Wire accounts
| Status code | Error code | Description |
| ----------- | ---------- | ----------------------------------------------------------------------------------------------------------- |
| `400` | `1106` | Set `billingDetails.district` for addresses in the United States or Canada. Use the state or province code. |
## Idempotency
An idempotency key stays bound to its record and doesn't expire. The same body
returns the first record. A new body returns `409` with code `1083`. Use a new
UUID for each new request.
# Stablecoin payin states
Source: https://developers.circle.com/cpn/managed-payments/references/payment-intent-states
Reference for PaymentIntent, Payment, and Refund states in the Managed Payments stablecoin payin flow.
Managed Payments tracks stablecoin payins using three resources:
`PaymentIntent`, `Payment`, and `Refund`. Each resource moves through a set of
states as the payin progresses.
## PaymentIntent states
A `PaymentIntent` is created when you request a deposit address. Its state shows
whether the address is ready and whether funds have settled.
| State | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `pending` | The deposit address is being provisioned and is not yet ready. |
| `paid` | At least one payment has settled. For continuous intents, monitor individual `Payment` objects for each settlement. Terminal state. |
| `failed` | The intent could not be fulfilled. Terminal state. |
| `expired` | The `expiresOn` time passed with no payment received. No further payments are accepted. Applies to transient intents only. Terminal state. |
Transient intents expire when the `expiresOn` timestamp is reached with no
payment received. Continuous intents do not expire.
## Payment states
A `Payment` is created each time funds arrive at the deposit address. Its state
shows whether the payment settled.
| State | Description |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| [`pending`](/cpn/managed-payments/references/webhook-events#payment-events) | An onchain payment was detected and settlement is in progress. |
| [`paid`](/cpn/managed-payments/references/webhook-events#payment-events) | The payment settled successfully. Terminal state. |
| `failed` | The payment failed. Check `statusReason` for the reason. Terminal state. |
## Refund states
A `Refund` is created when funds from a `Payment` are returned to the sender.
Its state shows whether the refund was delivered.
| State | Description |
| --------- | --------------------------------------------------------------------------------------- |
| `pending` | The refund was initiated and settlement is in progress. |
| `paid` | The refund was delivered successfully. Terminal state. |
| `failed` | The refund could not be delivered. Check `statusReason` for the reason. Terminal state. |
## Status reasons
The `statusReason` field provides additional context for certain `Payment`
states.
| Status | `statusReason` | Description |
| --------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
| `pending` | `compliance_review` | Compliance is awaiting PII before releasing funds. |
| `failed` | `compliance_denied` | Funds seized due to sanctions hit. |
| `failed` | `unsupported_wallet` | Funds are stuck in escrow because the sender wallet is not supported (self-hosted wallet or non-VASP). |
| `paid` | `unsupported_wallet` | Funds settled but were received from an unsupported VASP. |
States with a link in the Payment states table have a corresponding webhook
event. `failed` has no webhook.
# Reporting
Source: https://developers.circle.com/cpn/managed-payments/references/reporting
Reference for Managed Payments reporting: the report types available through the /v1/reports API, how to request and download them, and how they differ from Circle Mint reporting.
Managed Payments generates reports through the core Circle Reports API
([`/v1/reports`](/api-reference/circle-mint/account/request-report)). The
following report types cover Managed Payments activity only. They're separate
from the Circle Mint `camt053` daily statement, which isn't available on Managed
Payments keys.
## Report types
| `reportType` | Contents | `timeframe` | Format |
| --------------------------------- | --------------------------------------------------------------------- | ------------------ | ------ |
| `managed_payin_transactions` | Payin (deposit) transactions for the period | `daily`, `monthly` | CSV |
| `managed_payout_transactions` | Payout transactions for the period | `daily`, `monthly` | CSV |
| `managed_payment_wallet_balances` | Wallet and subaccount balance snapshot | `daily` | CSV |
| `camt_managed` | ISO 20022 CAMT.053 bank-to-customer statement, Managed Payments scope | `daily` | XML |
Use `camt_managed` for CAMT.053 statements on Managed Payments. The Circle
Mint `camt053` type isn't valid on Managed Payments keys. It covers Circle
Mint accounts, not Managed Payments. See [CAMT.053 daily
statements](/circle-mint/references/camt053-statements) for the ISO 20022 XML
structure, which `camt_managed` shares.
## Request and download a report
1. Request the report with
[`POST /v1/reports`](/api-reference/circle-mint/account/request-report),
passing `reportType`, `timeframe` where the type supports it, and `date`.
Format `date` to match the timeframe: daily reports use `YYYY-MM-DD` (for
example, `2026-06-30`), and the monthly `managed_payin_transactions` and
`managed_payout_transactions` reports use `YYYY-MM` (for example, `2026-06`).
Passing a full date for a monthly report returns `400`. Circle interprets
`date` in your entity's reporting timezone, not UTC. For example, a Circle SG
entity uses Singapore Time (SGT). Circle generates Managed Payments reports
on a schedule:
* If the report is available, the response is `200 OK` with the report `id`
and a pre-signed `downloadUrl`.
* If the report is still being generated, the response is `202 Accepted` with
a report `id` and no `downloadUrl`.
2. If the response was `202 Accepted`, poll
[`GET /v1/reports/{id}`](/api-reference/circle-mint/account/get-report-by-id)
until `status` is `ready` and a `downloadUrl` is present.
3. Download the report. Either fetch the file from `downloadUrl` before the time
given by `expiresAt`, or stream it directly from
[`GET /v1/reports/{id}/content`](/api-reference/circle-mint/account/get-report-content),
which returns the raw file and responds with `409` if the report isn't
`ready`. After a `downloadUrl` expires, call `GET /v1/reports/{id}` for a
fresh URL. You can also request the report again. Requests are idempotent, so
the same parameters return the same report.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/reports \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"reportType":"camt_managed","timeframe":"daily","date":"2026-06-30"}'
```
```json theme={null}
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"reportType": "camt_managed",
"status": "ready",
"downloadUrl": "https://s3.amazonaws.com/...",
"expiresAt": "2026-06-30T15:00:00Z"
}
}
```
`expiresAt` uses different formats across endpoints. Create (`POST
/v1/reports`) and get-by-ID (`GET /v1/reports/{id}`) responses return it as an
ISO 8601 timestamp, as in the preceding example. The list endpoint (`GET
/v1/reports`) returns it as epoch milliseconds.
## List generated reports
To find reports you've already generated, use
[`GET /v1/reports`](/api-reference/circle-mint/account/list-reports) with a
`reportType` and a date range. Each report in the response carries its own
pre-signed `downloadUrl`. The list endpoint doesn't return the Circle Mint
`camt053` statement. Request that report with `POST /v1/reports` instead.
The `id` returned by `GET /v1/reports` is a storage object key (for example,
`managed_payout_transactions/{UUID}/{date}/report.csv`), not the deterministic
report UUID. You can't pass it to `GET /v1/reports/{id}` or `GET /v1/reports/ {id}/content`. Download a listed report from its own `downloadUrl`.
# Webhook events
Source: https://developers.circle.com/cpn/managed-payments/references/webhook-events
Managed Payments webhook event types, payloads, and v1 subscriptions
Managed Payments uses the v1 Circle APIs notification subscription system,
which is separate from the CPN Platform v2 webhook system. For CPN Platform
webhook events, see the [CPN Platform webhook events
reference](/cpn/references/webhooks/webhook-events).
See
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications)
to register an endpoint. The
[webhooks overview](/api-reference/webhooks#event-model) defines the v1
envelope.
## Event types
| `notificationType` | Resource key | Trigger |
| ----------------------- | ---------------------- | ------------------------------------------------------------------ |
| `paymentIntents` | `paymentIntent` | An intent is created, gets an address, or receives funds. |
| `payments` | `payment` | A payment status changes. |
| `addressBookRecipients` | `addressBookRecipient` | A recipient status changes. |
| `payouts` | `payout` | A stablecoin payout or fiat withdrawal status changes. |
| `wireTransfer` | `wireTransfer` | A fiat withdrawal (wire) status changes. |
| `creditTransfers` | `creditTransfer` | A credit transfer status changes. |
| `wire` | `wire` | A wire bank account status changes. |
| `deposits` | `deposit` | A fiat deposit settles to a wallet or subaccount. |
| `accounts` | `account` | A subaccount status changes to `pending`, `active`, or `rejected`. |
## Payment intent events
The `paymentIntents` event fires when an intent is created or gets a deposit
address. It also fires when the intent receives funds.
`timeline[].status` values:
* `created`: intent created
* `pending`: awaiting deposit; one-time intents use this state
* `active`: deposit address assigned; continuous intents use this in place of
`pending`
* `complete`: funds received in full
* `expired`: intent expired before completion
* `failed`: intent could not be fulfilled
* `refunded`: funds returned to the sender
```json Example paymentIntents payload theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"paymentIntent": {
"type": "continuous",
"id": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
}
],
"timeline": [
{
"status": "active",
"time": "2026-03-23T17:41:23.450386Z"
},
{
"status": "created",
"time": "2026-03-23T17:41:19.986668Z"
}
],
"createDate": "2026-03-23T17:41:19.979669Z",
"updateDate": "2026-03-23T17:41:23.405690Z",
"merchantWalletId": "12345",
"currency": "USD"
}
}
```
## Payment events
The `payments` event fires when a payment status changes.
`payment.status` values:
* `pending`: payment detected, awaiting confirmation
* `paid`: funds settled to the merchant wallet
* `failed`: payment could not be completed
```json Example payments payload theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "payments",
"version": 1,
"customAttributes": {
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291"
},
"payment": {
"id": "021ff661-e7d5-332f-bb9c-e43870608f26",
"type": "payment",
"status": "pending",
"amount": {
"amount": "5.00",
"currency": "USD"
},
"createDate": "2026-03-23T17:49:54.197Z",
"updateDate": "2026-03-23T17:49:54.259Z",
"merchantId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"merchantWalletId": "12345",
"paymentIntentId": "e7b49cb6-1f78-4a0f-8fd6-35fc74dca335",
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x6dbe810e3314546009bd6e1b29f9031211cda5d2"]
},
"depositAddress": {
"chain": "ETH",
"address": "0xfd5a9f666d96022d13a73e3638fb7ec958696fbe"
},
"transactionHash": "0xfbc0f1c8256af3453fd3be7a1491e3581e072022a29ffc78cf129a662182305e"
}
}
```
## Address book recipient events
The `addressBookRecipients` event fires when a recipient's status changes.
`addressBookRecipient.status` values:
* `pending`: recipient submitted, awaiting review
* `active`: recipient approved
* `inactive`: recipient deactivated
* `denied`: recipient rejected
* `failed`: verification failed
* `pending_verification`: additional verification in progress
* `on_hold`: review paused pending more information
```json Example addressBookRecipients payload theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"addressBookRecipient": {
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "active",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
## Payout events
The `payouts` event fires when a payout status changes. It covers both
stablecoin payouts and fiat withdrawals. Read `destination.type` to tell them
apart: an `address_book` or blockchain destination is a stablecoin payout, and a
`wire` destination is a fiat withdrawal. A fiat withdrawal also carries a `fees`
object and, if the bank returns the funds, a `return` object.
`payout.status` values:
* `pending`: payout initiated, in progress
* `complete`: funds delivered to the destination
* `failed`: payout could not be completed
```json Example payouts payload theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payouts",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-0a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2020-04-10T02:13:30.000Z",
"updateDate": "2020-04-10T02:13:30.000Z"
}
}
```
A fiat withdrawal carries a bank `destination` instead:
```json Example fiat withdrawal payload theme={null}
{
"clientId": "00000000-0000-0000-0000-000000000001",
"notificationType": "payouts",
"version": 1,
"customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" },
"payout": {
"id": "7a2a0284-d705-4311-af07-bc3a1ba93b6f",
"status": "complete",
"sourceWalletId": "1000038499",
"source": { "type": "wallet", "id": "1000038499" },
"destination": {
"type": "wire",
"id": "8c33b3eb-67a4-4f3d-9f4e-2d8a4f1c2b6a",
"name": "STANDARD CHARTERED BANK ****0010"
},
"amount": { "amount": "1000.00", "currency": "USD" },
"toAmount": { "amount": "1000.00", "currency": "USD" },
"fees": { "amount": "0.00", "currency": "USD" },
"trackingRef": "CIR2VKZ9G6",
"createDate": "2026-01-15T18:23:44.000Z",
"updateDate": "2026-01-15T18:26:02.000Z"
}
}
```
The `payouts` event fires only when a payout reaches a terminal status
(`complete` or `failed`), not while it's `pending`.
## Wire transfer events
The `wireTransfer` event fires when a fiat withdrawal changes status. Circle
delivers it as a webhook to Managed Payments accounts, alongside the `payouts`
event for the same withdrawal. It's separate from the
[`wire` event](#wire-bank-account-events), which reports bank account link
status.
`wireTransfer.status` values:
* `pending`: withdrawal initiated, in progress
* `complete`: funds sent to the bank
* `failed`: withdrawal could not be completed
```json Example wireTransfer payload theme={null}
{
"clientId": "00000000-0000-0000-0000-000000000001",
"notificationType": "wireTransfer",
"version": 1,
"customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" },
"wireTransfer": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"amount": { "amount": "1000.00", "currency": "USD" },
"toAmount": { "amount": "1000.00", "currency": "USD" },
"fees": { "amount": "0.00", "currency": "USD" },
"totalAmount": { "amount": "1000.00", "currency": "USD" },
"transactionType": "wire_withdrawal",
"status": "complete",
"location": {
"type": "wire",
"id": "8c33b3eb-67a4-4f3d-9f4e-2d8a4f1c2b6a",
"name": "STANDARD CHARTERED BANK ****0010"
},
"fiatTransferType": "wire",
"trackingRef": "CIR2VKZ9G6",
"sourceLocation": { "type": "wallet", "id": "1000038499" },
"createDate": 1784231462474,
"updateDate": 1784231534908
}
}
```
## Credit transfer events
The `creditTransfers` event fires when a credit transfer status changes. A
credit transfer draws funds from a line of credit.
`creditTransfer.status` values:
* `requested`: funds requested from the line of credit
* `disbursed`: funds sent to the wallet
* `paid`: transfer repaid in full
```json Example creditTransfers payload theme={null}
{
"clientId": "a49f9b1d-75e0-44a9-b8d2-4293b3f11ebd",
"notificationType": "creditTransfers",
"version": 1,
"customAttributes": {
"clientId": "a49f9b1d-75e0-44a9-b8d2-4293b3f11ebd"
},
"creditTransfer": {
"id": "0c206348-1252-4c33-8618-5d85e322a0a3",
"amount": {
"amount": "50.00",
"currency": "USD"
},
"status": "disbursed",
"outstanding": {
"amount": "50.00",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"dueDate": "2026-02-27T19:37:58.954Z",
"disbursedDate": "2026-02-20T19:37:58.954Z",
"createDate": "2026-02-20T19:37:58.358590Z",
"updateDate": "2026-02-20T19:37:59.321362Z"
}
}
```
### Credit transfer fields
The `creditTransfer` object has these fields:
| Field | Type | Description |
| --------------- | -------------------------------------- | ------------------------------------------------------- |
| `id` | string (UUID) | ID for the credit transfer. |
| `amount` | Amount object (`{ amount, currency }`) | Funds drawn from the line of credit. |
| `status` | enum: `requested`, `disbursed`, `paid` | Current transfer status. |
| `outstanding` | Amount object (`{ amount, currency }`) | Balance left to repay. Omitted for `requested`. |
| `fees` | Amount object (`{ amount, currency }`) | Fees due. Omitted for `requested`. |
| `dueDate` | ISO 8601 timestamp | Date payment is due. Omitted for `requested`. |
| `disbursedDate` | ISO 8601 timestamp | Date funds reached the wallet. Omitted for `requested`. |
| `paidDate` | ISO 8601 timestamp | Date the transfer was repaid. Omitted unless `paid`. |
| `createDate` | ISO 8601 timestamp | Date the transfer was created. |
| `updateDate` | ISO 8601 timestamp | Date the transfer last changed. |
## Wire bank account events
The `wire` event fires when a linked wire bank account changes status. Create
the account through the
[Wires API](/api-reference/cpn/managed-payments/wires/create-account-wire-account).
`wire.status` values:
* `pending`: Circle is verifying the account
* `complete`: account linked successfully
* `failed`: account could not be linked
```json Example wire payload 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": "STANDARD CHARTERED BANK ****0010",
"trackingRef": "CIR2VKZ9G6",
"createDate": "2026-01-15T18:23:44.000Z",
"updateDate": "2026-01-15T18:25:11.000Z"
}
}
```
## Deposit events
The `deposits` event fires when a fiat deposit settles to a wallet or
subaccount. The payload includes the deposit `id`, `amount`, `source`, and
`destination`.
`deposit.status` values:
* `pending`: deposit detected, settlement in progress
* `complete`: funds credited to the wallet or subaccount
* `failed`: deposit could not be settled
```json Example deposits payload 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" },
"fromAmount": { "amount": "1000.00", "currency": "USD" },
"fees": { "amount": "0.00", "currency": "USD" },
"source": { "type": "wire", "id": "8c33b3eb-67a4-4f3d-9f4e-2d8a4f1c2b6a" },
"destination": { "type": "wallet", "id": "1000038499" },
"trackingRef": "CIR2VKZ9G6",
"createDate": "2026-01-15T18:23:44.000Z",
"updateDate": "2026-01-15T18:26:02.000Z"
}
}
```
## Account events
The `accounts` event reports a subaccount's status. Use it to learn when a new
subaccount is ready.
`account.status` values:
* `pending`: subaccount created, compliance check in progress
* `active`: subaccount ready for use
* `rejected`: subaccount denied
This event reports the result of a compliance check. In most cases, it is
`active` or `rejected`.
```json Example accounts payload theme={null}
{
"clientId": "00000000-0000-0000-0000-000000000001",
"notificationType": "accounts",
"version": 1,
"customAttributes": { "clientId": "00000000-0000-0000-0000-000000000001" },
"account": {
"accountId": "1000038499",
"entityId": "9a4d6e72-78aa-4b9d-bd84-2cc7c1a8c2d4",
"clientEntityId": "00000000-0000-0000-0000-000000000001",
"type": "client",
"status": "active",
"description": "Example Trading Ltd."
}
}
```
# Integrate with CPN as an OFI
Source: https://developers.circle.com/cpn/quickstarts/integrate-with-cpn-ofi
Request, lock, and fulfill a USDC payment quote as an OFI using CPN. Covers Transactions V2 on EVM blockchains and Solana signing.
Complete this quickstart to request, lock, and fulfill your first USDC payment
quote as an OFI using CPN. Examples use Transactions V2 on EVM blockchains.
Solana follows the same overall flow with differences in the signing process.
This quickstart uses **Circle Wallets** and **Circle On/Off-Ramps** as
examples; CPN does not require them. If you use your own wallet and USDC, see
[Bring your own wallet for CPN](/cpn/concepts/wallets/bring-your-own-wallet)
and [Wallet provider
compatibility](/cpn/references/blockchains/wallet-provider-compatibility). For
Circle setup, follow the how-tos under Prerequisites.
## Prerequisites
Before you begin this quickstart, ensure you have:
* API keys created in
[CPN Console → Developer → API Keys](https://cpn.circle.com/signin) with
access to **CPN** and **Programmable Wallets**. If you fund USDC through
Circle APIs for this exercise, you also need a key authorized for those
**Circle APIs** endpoints.
* Completed operational wallet setup for the quickstart:
* Follow how to
[Set Up a Circle Wallet for CPN Payments](/cpn/guides/wallets/setup-circle-wallet-for-cpn-payments),
**or** use your own wallet that meets
[Wallet provider compatibility](/cpn/references/blockchains/wallet-provider-compatibility).
* You need the **wallet ID** of an EOA on the correct chain (`ETH-SEPOLIA`
when using testnet).
* Funded the operational wallet with enough **USDC**. For sandbox you can use
the [Circle faucet](https://faucet.circle.com). For a guided Circle
fiat-to-wallet path, follow how to
[Set Up Circle On/Off-Ramps for CPN Payments](/cpn/guides/circle-liquidity/setup-circle-on-off-ramps-for-cpn-payments).
* Python installed on your development machine
* The latest `jwcrypto`, `web3`, `eth_utils`, `hexbytes`, and `eth_abi`
libraries are installed with the `pip` package manager
* cURL installed on your development machine
* Granted a USDC allowance to the `Permit2` contract. See how to
[Grant USDC allowance to Permit2](/cpn/guides/transactions/grant-usdc-allowance-to-permit2)
for more information.
* (Optional) a
[configured webhook notification endpoint](/api-reference/webhook-endpoints)
This quickstart provides API requests in cURL format, along with example
responses.
The base URL for all API endpoints is `https://api.circle.com/v1/cpn` for both
sandbox and production environments. The API determines if a request is for
testnet or mainnet based on the key used to authenticate the request.
## Part 1: Request a quote
Request quotes for a USDC to MX payment with the SPEI payment method. Request
quotes with the [create a quote](/api-reference/cpn/cpn-platform/create-quotes)
endpoint, providing the source currency and destination amount. You must specify
`transactionVersion` as `VERSION_2`. The endpoint returns a list of quotes from
various BFIs with the rate, expiration time, USDC settlement window, and unique
ID.
The `senderType` and `recipientType` you request the quote with also determine
the quote's [payment use case](/cpn/concepts/quotes#payment-use-case). You can't
set the use case on the quote, and the quote response doesn't return it, so keep
track of the types you requested. When you create the payment in Part 2.3, you
must send the `useCase` that matches:
| `senderType` | `recipientType` | `useCase` |
| ------------ | --------------- | --------- |
| `BUSINESS` | `BUSINESS` | `B2B` |
| `BUSINESS` | `INDIVIDUAL` | `B2C` |
| `INDIVIDUAL` | `BUSINESS` | `C2B` |
| `INDIVIDUAL` | `INDIVIDUAL` | `C2C` |
A payment that sends any other value is rejected with error code
[`290211`](/api-reference/cpn/error-codes). This quickstart uses `INDIVIDUAL`
for both types, so its payment sends `C2C`.
Quotes expire quickly (typically 30–60 seconds). If a quote expires before you
create the payment, the payment creation endpoint returns an error. Request a
new quote and restart from Part 1. Save the `id` and `quoteExpireDate` from
the response so you can check expiration before proceeding.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/cpn/quotes \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"paymentMethodType": "SPEI",
"senderCountry": "US",
"destinationCountry": "MX",
"sourceAmount": {
"currency": "USDC"
},
"destinationAmount": {
"amount": "200",
"currency": "MXN"
},
"blockchain": "ETH-SEPOLIA",
"senderType": "INDIVIDUAL",
"recipientType": "INDIVIDUAL",
"transactionVersion": "VERSION_2"
}
'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "2792f4a6-f1bd-4435-b681-1da309122159",
"paymentMethodType": "SPEI",
"blockchain": "ETH-SEPOLIA",
"senderCountry": "US",
"destinationCountry": "MX",
"createDate": "2025-09-24T00:01:13.532073875Z",
"quoteExpireDate": "2025-09-24T00:01:42.502094Z",
"cryptoFundsSettlementExpireDate": "2025-09-24T01:01:12.502097Z",
"sourceAmount": {
"amount": "15.000000",
"currency": "USDC"
},
"destinationAmount": {
"amount": "252.91",
"currency": "MXN"
},
"fiatSettlementTime": {
"min": "0",
"max": "5",
"unit": "MINUTES"
},
"exchangeRate": {
"rate": "16.860667",
"pair": "USDC/MXN"
},
"fees": {
"totalAmount": {
"amount": "1.568971",
"currency": "USDC"
},
"breakdown": [
{
"type": "TAX_FEE",
"amount": {
"amount": "0.234663",
"currency": "USDC"
}
},
{
"type": "BFI_TRANSACTION_FEE",
"amount": {
"amount": "0.138037",
"currency": "USDC"
}
},
{
"type": "CIRCLE_SERVICE_FEE",
"amount": {
"amount": "0.000000",
"currency": "USDC"
}
},
{
"type": "BLOCKCHAIN_GAS_FEE",
"amount": {
"amount": "1.196271",
"currency": "USDC"
}
}
]
},
"senderType": "INDIVIDUAL",
"recipientType": "INDIVIDUAL",
"certificate": {
// certificate object
},
"quoteOptions": {
"isFirstParty": false
},
"transactionVersion": "VERSION_2"
}
]
}
```
## Part 2: Create a payment
Use the API to get the requirements for a payment, accept the quote, and create
a payment.
### 2.1. Get payment requirements
Call the
[`/payments/requirements`](/api-reference/cpn/cpn-platform/get-payment-requirements)
endpoint with the quote ID to get the requirements for a payment. The endpoint
returns an object describing the required fields for the compliance check. The
`optional` field for each parameter defines if the parameter must be included in
the response constructed in the next step.
```shell theme={null}
curl -H "Authorization: Bearer ${YOUR_API_KEY}" \
-X GET "https://api.circle.com/v1/cpn/payments/requirements?quoteId=${QUOTE_ID}"
```
**Response**
```json theme={null}
{
"data": {
"travelRule": [
{
"name": "ORIGINATOR_FINANCIAL_INSTITUTION_NAME",
"type": "TEXT",
"optional": false
},
{
"name": "ORIGINATOR_FINANCIAL_INSTITUTION_ADDRESS",
"type": "ADDRESS",
"optional": false
},
{
"name": "ORIGINATOR_FINANCIAL_INSTITUTION_ID",
"type": "TEXT",
"optional": true
},
{
"name": "ORIGINATOR_NAME",
"type": "TEXT",
"optional": false
},
{
"name": "ORIGINATOR_ACCOUNT_NUMBER",
"type": "TEXT",
"optional": false
},
{
"name": "ORIGINATOR_ADDRESS",
"type": "ADDRESS",
"optional": false
},
{
"name": "BENEFICIARY_NAME",
"type": "TEXT",
"optional": false
},
{
"name": "BENEFICIARY_ADDRESS",
"type": "ADDRESS",
"optional": false
},
{
"name": "ORIGINATOR_DATE_OF_BIRTH",
"type": "TEXT",
"optional": true
},
{
"name": "ORIGINATOR_NATIONALITY",
"type": "TEXT",
"optional": true
},
{
"name": "ORIGINATOR_NATIONAL_IDENTIFICATION_NUMBER",
"type": "TEXT",
"optional": true
},
{
"name": "BENEFICIARY_DATE_OF_BIRTH",
"type": "TEXT",
"optional": true
},
{
"name": "BENEFICIARY_NATIONALITY",
"type": "TEXT",
"optional": true
},
{
"name": "BENEFICIARY_NATIONAL_IDENTIFICATION_NUMBER",
"type": "TEXT",
"optional": false
},
{
"name": "BENEFICIARY_PHONE_NUMBER",
"type": "TEXT",
"optional": true
},
{
"name": "BENEFICIARY_EMAIL",
"type": "TEXT",
"optional": true
}
],
"beneficiaryAccount": [
{
"name": "CLABE",
"type": "TEXT",
"optional": false
}
]
}
}
```
### 2.2. Encrypt the required fields
Construct a JSON object with the information requested in the previous step. For
each schema, the properties that you must include are outlined by the `optional`
field. Encrypt the object with the `jwk` certificate provided in the quote
response.
The correct format for travel rule data and beneficiary account data is a JSON
array of objects where each object contains two properties: `name` and `value`.
You can review an example of each field in how to
[Encrypt Travel Rule Data](/cpn/guides/payments/encrypt-travel-rule-beneficiary-data#step-4-prepare-the-payload).
Create a file called `cpn_encryption.py` and put the following code in it,
replacing the `requirements_response_json` parameter with the contents of the
response from the previous step, and the `certificate_json` parameter with the
`jwk` from the quote response. When you run the script, it outputs the encrypted
beneficiary and travel rule data to the console.
```python Python theme={null}
"""
CPN Requirements Encryption Quickstart
This script demonstrates how to:
1. Parse the Requirements response
2. Generate realistic test data matching the fields
3. Encrypt data using JWE for CPN API integration
Usage:
1. Replace certificate_json with your JWK from Quote response
2. Replace requirements_response_json with your Requirements response
3. Run the script to get encrypted data for creating payment API requests
"""
import json
import os
import base64
import random
from typing import Dict, Any, Optional, List
from jwcrypto import jwk, jwe
# ========================================
# Test Data Lists for Realistic Generation
# ========================================
FIRST_NAMES = [
"James", "John", "Robert", "Michael", "William", "David", "Joseph", "Thomas",
"Charles", "Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara",
"Susan", "Jessica", "Sarah", "Karen", "Nancy"
]
LAST_NAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson",
"Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"
]
STREET_TYPES = ["St", "Ave", "Blvd", "Rd", "Ln", "Dr", "Way", "Circle", "Court"]
STREET_NAMES = [
"Main", "Oak", "Maple", "Cedar", "Pine", "Elm", "Washington", "Lake", "Hill",
"River", "Valley", "Park", "Spring", "Market", "Church", "Bridge", "Highland"
]
CITIES = [
"New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia",
"San Antonio", "San Diego", "Dallas", "San Jose", "Austin", "Jacksonville",
"Fort Worth", "Columbus", "San Francisco", "Charlotte", "Indianapolis",
"Seattle", "Denver", "Washington"
]
# ========================================
# Helper Functions
# ========================================
def generate_random_name() -> str:
"""Generate a random realistic name."""
return f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
def generate_random_address() -> Dict[str, str]:
"""Generate a random realistic address."""
street_number = str(random.randint(1, 9999))
street_name = random.choice(STREET_NAMES)
street_type = random.choice(STREET_TYPES)
return {
"street": f"{street_number} {street_name} {street_type}",
"city": random.choice(CITIES),
"country": "US",
"postalCode": f"{random.randint(10000, 99999)}"
}
def random_string(length: int = 12) -> str:
"""Generate a random string of given length."""
return base64.b64encode(os.urandom(length)).decode()[:length]
def get_originator_name(case: Optional[str] = None) -> str:
"""Get the originator name based on test case."""
if case == 'rfi-failed':
return "Failed"
return "Alice Johnson" # Default for success case
# ========================================
# Core Data Generation
# ========================================
def generate_group_data(fields: List[Dict[str, Any]], originator_name: str) -> List[Dict[str, Any]]:
"""
Generate test data matching Requirements fields as an array of {name, value}.
Args:
fields: List of field objects from Requirements
originator_name: Name to use for originator fields
Returns:
List[{"name": str, "value": Any}] for required fields
"""
items: List[Dict[str, Any]] = []
for field in fields:
name = field["name"]
field_type = field["type"].upper()
optional = field.get("optional", False)
if optional:
continue # only include required fields
# Address fields
if field_type == "ADDRESS" or "ADDRESS" in name.upper():
items.append({"name": name, "value": generate_random_address()})
# Text fields
elif field_type == "TEXT":
if "NAME" in name.upper():
if "ORIGINATOR" in name.upper():
items.append({"name": name, "value": originator_name})
else:
items.append({"name": name, "value": generate_random_name()})
elif "CLABE" in name.upper():
items.append({"name": name, "value": ''.join(str(random.randint(0, 9)) for _ in range(18))})
elif "ACCOUNT" in name.upper():
items.append({"name": name, "value": ''.join(str(random.randint(0, 9)) for _ in range(12))})
elif "DATE" in name.upper() or "BIRTH" in name.upper():
year = random.randint(1970, 2000)
month = random.randint(1, 12)
day = random.randint(1, 28)
items.append({"name": name, "value": f"{year:04d}-{month:02d}-{day:02d}"})
elif "EMAIL" in name.upper():
items.append({"name": name, "value": f"{random_string(8)}@example.com"})
else:
items.append({"name": name, "value": random_string(12)})
return items
# ========================================
# Encryption
# ========================================
def encrypt_data(data: Any, jwk_data: Dict[str, Any]) -> str:
"""
Encrypt data using JWE with provided JWK.
Args:
data: Data to encrypt (will be JSON serialized)
jwk_data: JWK from certificate
Returns:
Encrypted JWE string
"""
recipient_key = jwk.JWK(**jwk_data)
jwe_obj = jwe.JWE(
plaintext=json.dumps(data).encode(),
protected=json.dumps({"alg": "ECDH-ES+A128KW", "enc": "A128GCM"})
)
jwe_obj.add_recipient(recipient_key)
return jwe_obj.serialize(True)
# ========================================
# Configuration - Replace with your data
# ========================================
# Certificate JWK - copy from Quote response
# e.g. {"kty":"EC","crv":"P-256","kid":"263...5762","x":"Ydj...2Y","y":"n621...i8"}
certificate_json = '''certificate_json'''
# Requirements response - copy from Requirements API
# e.g. {"data": {"travelRule": [...], "beneficiaryAccount": [...]}}
requirements_response_json = '''requirements_response_json'''
# ========================================
# Main Execution
# ========================================
if __name__ == "__main__":
# Parse configuration
certificate = json.loads(certificate_json)
required_fields = json.loads(requirements_response_json)
# Extract field arrays
travel_rule_fields = required_fields['data']['travelRule']
beneficiary_account_fields = required_fields['data']['beneficiaryAccount']
# Generate test data (array of {name, value})
test_data = {
"travelRuleData": generate_group_data(travel_rule_fields, get_originator_name()),
"beneficiaryAccountData": generate_group_data(beneficiary_account_fields, get_originator_name())
}
# Create encrypted data (encrypt arrays directly)
travel_rule_encrypted = encrypt_data(test_data["travelRuleData"], certificate)
beneficiary_account_encrypted = encrypt_data(test_data["beneficiaryAccountData"], certificate)
# Output encrypted data ready for API
print(f"Travel Rule encryptedData: {travel_rule_encrypted}\n")
print(f"Beneficiary Account encryptedData: {beneficiary_account_encrypted}")
```
### 2.3. Create a payment
After the quote is accepted, create a payment by calling the
[`/payments`](/api-reference/cpn/cpn-platform/create-payment) endpoint. You need
to provide the quote ID, encrypted sender and receiver information, and a
`refundAddress`. The endpoint returns a unique payment ID and the initial status
of the payment.
The `refundAddress` is the wallet address that receives the funds if the payment
fails. It must be on the same blockchain as `senderAddress` but can be a
different wallet address.
Send the `useCase` that your quote's `senderType` and `recipientType` imply, as
described in [Part 1](#part-1-request-a-quote). The quote in this quickstart
uses `INDIVIDUAL` for both, so the payment sends `C2C`.
You must create the payment before the quote expires. If the quote has
expired, request a new quote and restart from Part 1. Once the payment is
created, save the payment `id` and `expireDate`. You must complete the onchain
transaction before the payment expires.
```bash Shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/cpn/payments \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-type: application/json' \
--data '
{
"idempotencyKey" : "${randomUUID}",
"quoteId" : "${cpn_ofi_quote_id}",
"beneficiaryAccountData" : "${encrypted_beneficiary_data}",
"travelRuleData" : "${encrypted_travel_rule_data}",
"senderAddress" : "${YOUR_WALLET_ADDRESS}",
"refundAddress" : "${YOUR_WALLET_ADDRESS}",
"blockchain" : "ETH-SEPOLIA",
"reasonForPayment" : "PMT001",
"customerRefId" : "123c7442-e843-4afa-bfad-35f50636d35b",
"refCode" : "7b479c5a-3684-4423-9fc6-f7c890c0e816",
"useCase" : "C2C"
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "07dbe320-6bcb-475b-8d21-17b57263cd3e",
"quoteId": "922a06cd-ff1e-4ee4-840e-54006893fd1a",
"blockchain": "ETH-SEPOLIA",
"paymentMethodType": "SPEI",
"sourceAmount": {
"amount": "10.000000",
"currency": "USDC"
},
"destinationAmount": {
"amount": "200.23",
"currency": "MXN"
},
"status": "CRYPTO_FUNDS_PENDING",
"refCode": "7b479c5a-3684-4423-9fc6-f7c890c0e816",
"customerRefId": "123c7442-e843-4afa-bfad-35f50636d35b",
"useCase": "C2C",
"expireDate": "2025-03-31T20:59:21.211547Z",
"createDate": "2025-03-31T18:59:30.183044Z",
"fees": {
"totalAmount": {
"amount": "0.170000",
"currency": "USDC"
},
"breakdown": [
{
"type": "TAX_FEE",
"amount": {
"amount": "0.070000",
"currency": "USDC"
}
},
{
"type": "BFI_TRANSACTION_FEE",
"amount": {
"amount": "0.100000",
"currency": "USDC"
}
}
]
},
"fiatSettlementTime": {
"min": "1",
"max": "12",
"unit": "HOURS"
},
"rfis": [],
"onChainTransactions": []
}
}
```
## Part 3: Create a transaction
Use the API to create a blockchain transaction to transfer USDC. Sign the
transaction locally, and use the API to broadcast it to the blockchain.
This quickstart uses Circle Wallets (or your equivalent operational wallet) as
the originator wallet. Use the wallet ID from the
[prerequisites](#prerequisites) section, including any funding you completed
via the Circle Wallet or Circle On/Off-Ramps how-tos.
### 3.1. Initiate the onchain transaction
Initiate the onchain funds transfer by calling the
[create transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint with the payment ID from the previous step, and other
transaction-related parameters. The endpoint returns an unsigned onchain
transaction object and a transaction ID.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v2/cpn/payments/:paymentId/transactions \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey" : "${RANDOM_UUID}"
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "dbc27d23-cd4f-447e-855e-349cb2853d23",
"status": "CREATED",
"paymentId": "49d4231e-6c4f-319e-946d-ed8c8bab5abc",
"expireDate": "2025-09-08T20:02:06.651391Z",
"blockchain": "ETH-SEPOLIA",
"senderAddress": "0x57414adbBbc4BBA36f1dE26b2dc1648b28ae7799",
"destinationAddress": "0xc75c3e371d617b3e60db1b6f3fa2f0689562e5a7",
"amount": {
"amount": "15.000000",
"currency": "USDC"
},
"messageType": "PAYMENT_SETTLEMENT_CONTRACT_V1_0_PAYMENT_INTENT",
"messageToBeSigned": {
"domain": {
"name": "Permit2",
"chainId": "11155111",
"verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
"message": {
"permitted": {
"token": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"amount": "14174474"
},
"spender": "0xe2B17D0C1736dc7C462ABc4233C91BDb9F27DD1d",
"nonce": "25668617285137697861288274946631174355105919960416755114569514179393151588120",
"deadline": "1757362866",
"witness": {
"from": "0x57414adbBbc4BBA36f1dE26b2dc1648b28ae7799",
"to": "0xc75c3e371d617b3e60db1b6f3fa2f0689562e5a7",
"value": 14174474,
"validAfter": "1757358106",
"validBefore": "1757361726",
"nonce": "0x38bfec2b230187932870d575132e8ae1f83b34c10e3bf6d64c377f0c13245718",
"beneficiary": "0x4f1c3a0359A7fAd8Fa8E9E872F7C06dAd97C91Fd",
"maxFee": "0",
"attester": "0x768919ef04853b5fd444ccff48cea154768a0291",
"requirePayeeSign": false
}
},
"primaryType": "PermitWitnessTransferFrom",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "PaymentIntent"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"PaymentIntent": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "validAfter",
"type": "uint256"
},
{
"name": "validBefore",
"type": "uint256"
},
{
"name": "nonce",
"type": "bytes32"
},
{
"name": "beneficiary",
"type": "address"
},
{
"name": "maxFee",
"type": "uint256"
},
{
"name": "requirePayeeSign",
"type": "bool"
},
{
"name": "attester",
"type": "address"
}
]
}
},
"metadata": {}
}
}
```
### 3.2 Sign the onchain transaction
The following steps are for EVM blockchains. For Solana, you would follow a
similar process with some differences in the signing. Refer to how to [Create
an Onchain Transaction](/cpn/guides/transactions/create-an-onchain-txn) for
more information.
Using the
[`/sign/typedData`](/api-reference/wallets/developer-controlled-wallets/sign-typed-data)
endpoint, input the `messageToBeSigned` object from the previous step along with
your entity secret and wallet ID. The transaction parameter should be
[stringified](https://jsonformatter.org/json-stringify-online) from the
`messageToBeSigned` field from the transaction response.
```bash Shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/developer/sign/typedData \
--header 'Accept: application/json' \
--header 'authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"entitySecretCiphertext": "qXnnGgbsU5lBUGiW9kp2/ltuvSSWW4qJ4/9VKuQT7wd6+ge2y7xqYnEc0pHbqLuj+YBDaPMfRUl1X+K1hbyiPTRVjCqHD5x3DyLtj8eTG/GmIimYfXOveXIJjsT95T8bI9uJ9kxygYAQbNev6wX993OYTYZ8D2PfVLUV3BicTSiClqhgSLW1Nh0qJ+TK0p2rOHs2HZkGA/WTv4SQv+uq//wEbUWFmrrD/ToTSuv3tMQvluCMYDF9xO/F6EoQwmP/XJCpPihGZuvrweTnhHbNWe5suvSSKpB+8Yo6f24ttNtCwvHrLBVaF6U9EZrCRpCydHJuuVBf5j7AD0JPC2DPFAG2p/Upq/KdzF1r8GJ4j2SsFLyzQEAw3ZAl623UiB/F3Szu2T/fYeF0rkfNt6tYKqmCmhvlzvn8BBkgIXsdcoEmNsf4x7b7UwPk9EloTibF4MhkGIW7jDHWWXlL3gKpGzMug+A2bIYdwUtqQ+u65pDi4+o+tuEH8MtM9Mmt3YaP2Zr40wj/uMnRv53hc+Apzsvh6UIsmliK2ldPyfXg77eDEzU7E228al/jIi2YQacQLNAAV870v3iKFB0PeWiUNtVlUdnqXmZkMA/bmg4TOo05ROGJWkfPVFWUNoocyEvCfEasj0ZflfbO8W2Q0M9BqhqjU/WHEBrYnF65ytY0A+8=",
"data": "{\"domain\":{\"chainId\":\"11155111\",\"name\":\"USDC\",\"verifyingContract\":\"0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238\",\"version\":\"2\"},\"message\":{\"from\":\"0x39fd73b03a01c6230b5e0d946e1960d79db44fd8\",\"nonce\":\"0x854f1f66cb7cb0e266e17a3715c24c8dae1eb540c4eb00a7a1b39f4bfa9bcf09\",\"to\":\"0x6734b39043f1029f8d5f1b6948d5417b75a72cf8\",\"validAfter\":\"1743522751\",\"validBefore\":\"1743530842\",\"value\":\"10000000\"},\"primaryType\":\"TransferWithAuthorization\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"TransferWithAuthorization\":[{\"name\":\"from\",\"type\":\"address\"},{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"validAfter\",\"type\":\"uint256\"},{\"name\":\"validBefore\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"bytes32\"}]}}",
"walletId": "${YOUR_CIRCLE_WALLET_ID}"
}
'
```
**Response**
```json JSON theme={null}
{
"signature": "0x905d70de3f1d9e86b982f6aee2755807fcd50a11cd9035bf47845c856be920fc3b7af8d06bf953bfdecdcea4cc9250aeaeb178b50116774d6bfab37bcc3757621c"
}
```
### 3.3. Submit the signed transaction
Use the
[`/v2/cpn/payments/{paymentId}/transactions/{transactionId}/submit`](/api-reference/cpn/cpn-platform/submit-transaction-v2)
endpoint to submit the transaction to be broadcast to the blockchain. You should
submit the EIP-712 typed data signature you obtained from the `/sign/typedData`
endpoint in [step 3.2.](#3-2-sign-the-onchain-transaction) as
`signedTransaction`.
```bash Shell theme={null}
curl --request POST \
--url https://api.circle.com/v2/cpn/payments/:paymentId/transactions/:transactionId/submit \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-type: application/json' \
--data '
{
"signedTransaction": "0x12b5fb72e99f9bb0300d2eb66a6d89dd5a667f43669893cf14bfcc390754dcb61b69f92cba598ec83a184e11c97e3bb9964a2bfd7a09688eee63f586ad9ccae21c"
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "5cae9e1c-f3e3-44e5-ac36-d78f4ff9c56e",
"status": "PENDING",
"paymentId": "2b2b314a-0c06-39bb-b111-506f56599a17",
"expireDate": "2025-11-12T00:08:42.000875Z",
"blockchain": "ETH-SEPOLIA",
"senderAddress": "0x57414adbBbc4BBA36f1dE26b2dc1648b28ae7799",
"destinationAddress": "0xded12af48fb343b446bcbe739c5211636896362b",
"amount": {
"amount": "11.948672",
"currency": "USDC"
},
"messageType": "PAYMENT_SETTLEMENT_CONTRACT_V1_0_PAYMENT_INTENT",
"messageToBeSigned": {
"domain": {
"name": "Permit2",
"chainId": "11155111",
"verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
},
"message": {
"permitted": {
"token": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"amount": "11948672"
},
"spender": "0x8ea7239f185CC32AB1Ff698f1b1A3aAB615D6d2c",
"nonce": "55519981872451242578307489093459806523820915276389791540432104685160022674073",
"deadline": "1762907262",
"witness": {
"from": "0x57414adbBbc4BBA36f1dE26b2dc1648b28ae7799",
"to": "0xded12af48fb343b446bcbe739c5211636896362b",
"value": 11048874,
"validAfter": "1762901905",
"validBefore": "1762906122",
"nonce": "0x7abf323679377bff4d064e663e44b7064985eab99693dec4a9e8a9f941a80a99",
"beneficiary": "0x8049E74C07A6BAdc8ddeB7C3530Ab9Af30037211",
"maxFee": "899798",
"attester": "0xcf9e077c75ce6bd22f48163e559d20b10708ae85",
"requirePayeeSign": false
}
},
"primaryType": "PermitWitnessTransferFrom",
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "PaymentIntent"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"PaymentIntent": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
},
{
"name": "validAfter",
"type": "uint256"
},
{
"name": "validBefore",
"type": "uint256"
},
{
"name": "nonce",
"type": "bytes32"
},
{
"name": "beneficiary",
"type": "address"
},
{
"name": "maxFee",
"type": "uint256"
},
{
"name": "requirePayeeSign",
"type": "bool"
},
{
"name": "attester",
"type": "address"
}
]
}
},
"encodedMessageToBeSigned": "0xabc6f65eb8b2c264ae486b7244e9ca887cd0f8bd29422f651042665c14974ef3",
"metadata": {},
"version": "VERSION_2"
}
}
```
`transactionHash` will be provided after transaction is in `COMPLETED` status.
You can monitor for the transactions webhook events to get the
`transactionHash`.
Once the onchain transaction is confirmed by the BFI, the BFI initiates a fiat
payout to the recipient. As the fiat payout progresses, the OFI is notified by
[webhook notifications](/api-reference/webhooks).
# Payment smart contract addresses
Source: https://developers.circle.com/cpn/references/blockchains/contract-addresses
Payment smart contract addresses for CPN
The following sections provide the addresses for the CPN payment smart contracts
on supported blockchains.
## Mainnet
| Blockchain | Settlement Contract Address |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| Ethereum | [`0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868`](https://etherscan.io/address/0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868) |
| Polygon PoS | [`0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868`](https://polygonscan.com/address/0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868) |
## Testnet
| Blockchain | Settlement Contract Address |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Arc Testnet | [`0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868`](https://testnet.arcscan.app/address/0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868) |
| Ethereum Sepolia | [`0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868`](https://sepolia.etherscan.io/address/0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868) |
| Polygon PoS Amoy | [`0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868`](https://amoy.polygonscan.com/address/0x355e0a2a4B7563e0E00C90deD9Aa914c119Ee868) |
# Supported blockchains
Source: https://developers.circle.com/cpn/references/blockchains/supported-blockchains
CPN is chain-agnostic and built for multichain flexibility. OFIs can select the
blockchains they operate on, based on their internal risk, compliance,
operational, and security requirements.
The following sections list the blockchains that CPN supports.
## Testnet
* Ethereum Sepolia
* Polygon PoS Amoy
* Solana Devnet
## Mainnet
* Ethereum
* Polygon PoS
* Solana
# Wallet provider compatibility
Source: https://developers.circle.com/cpn/references/blockchains/wallet-provider-compatibility
CPN allows you to bring your own wallet provider when integrating with the
network. Circle Wallets is an option for your integration, but not a
requirement. When bringing your own wallet provider, you should make sure that
they support the features and signing methods that are required to interact with
CPN.
## Requirements
The following features and signing methods are required for a wallet provider to
be able to integrate with CPN:
* **Custodial wallet support:** The provider must be able to sign transactions
for wallets without requiring user interaction.
* **EIP-712 signing:** The provider must support EIP-712 typed data. The
resulting signature can be used in one of the following ways:
* The signature can be verified by recovering the signer's address with the
hash of the typed data (on behalf of an EOA).
* The signature can be validated by the `isValidSignature` method defined in
EIP-1271 with the hash of the typed data (on behalf of an SCA).
**Note:** USDC implements EIP-3009 with the EIP-7598 extension, enabling both
EOAs and SCAs to authorize transfers via the EIP-3009 mechanism.
* SCA wallet interface: When using an SCA wallet as the sender wallet, the
wallet contract must implement the following two interfaces:
* EIP-165 Standard Interface Detection: The SCA wallet must correctly
implement the EIP-165 interface detection standard. The `supportsInterface`
function must return `true` for the EIP-1271 interface ID.
* EIP-1271 Standard Signature Validation for Contracts: The SCA wallet must
correctly implement the EIP-1271 interface, which defines the
`isValidSignature` function.
* **Solana signing:** The provider must support signing raw Solana transaction
using `Ed25519`, without broadcasting them to the blockchain. The provider
must also support
[partial signing](https://solana.com/developers/cookbook/transactions/offline-transactions#partial-sign-transaction)
of the transaction.
* **Solana memo field:** The provider must support a memo field for Solana
transactions.
| Blockchain | Required signing methods |
| ---------- | -------------------------------------------- |
| EVM | EIP-712 typed data signing |
| Solana | Solana transaction signing (partial signing) |
# RFI levels
Source: https://developers.circle.com/cpn/references/compliance/rfi-levels
Requests for information (RFI) can have one of three levels, which indicate the
required information that must be passed to satisfy the RFI. The levels vary
depending on if the sender is an individual or a business.
## Individual
The following sections outline the requirements for each level if the sender is
an individual (natural person).
### Level 1
Level 1 requires the following information:
| Field | Description |
| -------------------------------- | -------------------------------------------------------------- |
| `ADDRESS` | Individual's full address |
| `NAME` | Individual's full name |
| `DATE_OF_BIRTH` | Individual's date of birth |
| `NATIONAL_IDENTIFICATION_NUMBER` | Unique government-issued ID (tax ID, national ID, or other) |
| `SOURCE_OF_FUNDS` | Source of funds for transactions |
| `METHOD_OF_VERIFICATION` | The method of verification (electronic, by document, or other) |
### Level 2
Level 2 requires all of the information required for level 1, plus the following
information:
| Field | Description |
| ------------- | ----------------------------------- |
| `NATIONALITY` | Nationality of the individual |
| `EMAIL` | Contact email address |
| `PHONE` | Primary phone number |
| `OCCUPATION` | Employment or professional activity |
### Level 3
Level 3 requires all the information required for levels 1 and 2, plus the
following information:
| Field | Description |
| ------------------ | ------------------------------------------------------ |
| `ID_DOC_TYPE` | Type of ID provided (passport, national ID, or others) |
| `ID_DOCUMENT` | Copy of the provided ID |
| `PROOF_OF_ADDRESS` | Utility bill, bank statement, or lease agreement |
| `ADDITIONAL_DOCS` | Any additional documents for enhanced due diligence |
## Business
The following sections outline the requirements for each level if the sender is
a business.
### Level 1
Level 1 requires the following information:
| Field | Description |
| -------------------------------- | ---------------------------------------------------------- |
| `NAME` | Legal registered name of the business |
| `TRADE_NAME` | Doing Business As (DBA) or trade name |
| `NATIONAL_IDENTIFICATION_NUMBER` | Business identification number (tax ID) |
| `DATE_OF_FORMATION` | Date of company formation |
| `COUNTRY_OF_FORMATION` | Country where the entity was formed |
| `ENTITY_TYPE` | Business structure (LLC, corporation, partnership, others) |
| `INDUSTRY_TYPE` | Classification of business activity |
| `ADDRESS` | Registered place of business |
| `METHOD_OF_VERIFICATION` | The method of verification |
| `SOURCE_OF_FUNDS` | Business funding sources |
### Level 2
Level 2 requires all of the information required for level 1, plus the following
information:
| Field | Description |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `AUTHORIZED_SIGNATORIES` | List of individuals with signature authorization |
| `BENEFICIARY_OWNERSHIP` | Whether any individuals or entities have significant ownership (directly or indirectly own >= 25%) of the company |
| `BENEFICIARY_OWNERS` | List of individuals with significant ownership (directly or indirectly owning >= 25%) of the company |
| `INTERMEDIARY_BENEFICIARY_OWNERS` | List of intermediary beneficiary owners with significant ownership (directly or indirectly own >= 25%) of the company |
| `WEBSITE` | Business website URL |
| `EMAIL` | Business contact email |
| `PHONE` | Business contact phone number |
### Level 3
Level 3 requires all the information required for levels 1 and 2, plus the
following information:
| Field | Description |
| -------------------------------------- | ------------------------------------------------------ |
| `FORMATION_DOCUMENT` | Articles of incorporation or certificate of formation |
| `PROOF_OF_ADDRESS` | Utility bill, lease agreement, or bank statement |
| `ORG_STRUCTURE` | Document of the organization structure of the business |
| `INVOICE` | Underlying invoice for the payment |
| `BENEFICIAL_OWNERS_IDENTITY_DOCUMENTS` | Beneficial owners ID documents |
# CPN supported countries
Source: https://developers.circle.com/cpn/references/compliance/supported-countries
Countries supported by the Circle Payments Network
The following table shows countries where Circle Payments Network is allowed to
operate. The country code is used in the
[`senderCountry` parameter](/api-reference/cpn/cpn-platform/create-quotes#body-sender-country)
in the CPN API. Note that the not all countries are currently supported by CPN.
Consult the preceding link for the full list of supported countries in the API
reference.
| Country | Country Code | Supported? |
| -------------------------------------------- | ------------ | ---------- |
| Afghanistan | AF | ❌ |
| Albania | AL | ✅ |
| Algeria | DZ | ✅ |
| American Samoa | AS | ✅ |
| Andorra | AD | ✅ |
| Angola | AO | ✅ |
| Anguilla | AI | ✅ |
| Antigua and Barbuda | AG | ✅ |
| Argentina | AR | ✅ |
| Armenia | AM | ✅ |
| Aruba | AW | ✅ |
| Australia | AU | ✅ |
| Austria | AT | ✅ |
| Azerbaijan | AZ | ✅ |
| Bahamas | BS | ✅ |
| Bahrain | BH | ✅ |
| Bangladesh | BD | ✅ |
| Barbados | BB | ✅ |
| Belarus | BY | ❌ |
| Belgium | BE | ✅ |
| Belize | BZ | ✅ |
| Benin | BJ | ✅ |
| Bermuda | BM | ✅ |
| Bhutan | BT | ✅ |
| Bolivia | BO | ✅ |
| Bonaire | BQ | ✅ |
| Bosnia and Herzegovina | BA | ✅ |
| Botswana | BW | ✅ |
| Bouvet Island | BV | ✅ |
| Brazil | BR | ✅ |
| British Indian Ocean Territory | IO | ✅ |
| Brunei Darussalam | BN | ✅ |
| Bulgaria | BG | ✅ |
| Burkina Faso | BF | ✅ |
| Burundi | BI | ✅ |
| Cabo Verde | CV | ✅ |
| Cambodia | KH | ✅ |
| Cameroon | CM | ✅ |
| Canada | CA | ✅ |
| Cayman Islands | KY | ✅ |
| Central African Republic | CF | ❌ |
| Chad | TD | ✅ |
| Chile | CL | ✅ |
| China | CN | ✅ |
| Christmas Island | CX | ✅ |
| Cocos (Keeling) Islands | CC | ✅ |
| Colombia | CO | ✅ |
| Comoros | KM | ✅ |
| Congo | CG | ✅ |
| Congo, Democratic Republic of the | CD | ❌ |
| Cook Islands | CK | ✅ |
| Costa Rica | CR | ✅ |
| Croatia | HR | ✅ |
| Cuba | CU | ❌ |
| Curacao | CW | ✅ |
| Cyprus | CY | ✅ |
| Czechia | CZ | ✅ |
| Cote d'Ivoire | CI | ✅ |
| Denmark | DK | ✅ |
| Djibouti | DJ | ✅ |
| Dominica | DM | ✅ |
| Dominican Republic | DO | ✅ |
| Ecuador | EC | ✅ |
| Egypt | EG | ✅ |
| El Salvador | SV | ✅ |
| Equatorial Guinea | GQ | ✅ |
| Eritrea | ER | ✅ |
| Estonia | EE | ✅ |
| Eswatini | SZ | ✅ |
| Ethiopia | ET | ✅ |
| Falkland Islands (Malvinas) | FK | ✅ |
| Faroe Islands | FO | ✅ |
| Fiji | FJ | ✅ |
| Finland | FI | ✅ |
| France | FR | ✅ |
| French Guiana | GF | ✅ |
| French Polynesia | PF | ✅ |
| French Southern Territories | TF | ✅ |
| Gabon | GA | ✅ |
| Gambia | GM | ✅ |
| Georgia | GE | ✅ |
| Germany | DE | ✅ |
| Ghana | GH | ✅ |
| Gibraltar | GI | ✅ |
| Greece | GR | ✅ |
| Greenland | GL | ✅ |
| Grenada | GD | ✅ |
| Guadeloupe | GP | ✅ |
| Guam | GU | ✅ |
| Guatemala | GT | ✅ |
| Guernsey | GG | ✅ |
| Guinea | GN | ✅ |
| Guinea-Bissau | GW | ❌ |
| Guyana | GY | ✅ |
| Haiti | HT | ✅ |
| Heard Island and McDonald Islands | HM | ✅ |
| Honduras | HN | ✅ |
| Hong Kong | HK | ✅ |
| Hungary | HU | ✅ |
| Iceland | IS | ✅ |
| India | IN | ✅ |
| Indonesia | ID | ✅ |
| Iran | IR | ❌ |
| Iraq | IQ | ❌ |
| Ireland | IE | ✅ |
| Isle of Man | IM | ✅ |
| Israel | IL | ✅ |
| Italy | IT | ✅ |
| Jamaica | JM | ✅ |
| Japan | JP | ✅ |
| Jersey | JE | ✅ |
| Jordan | JO | ✅ |
| Kazakhstan | KZ | ✅ |
| Kenya | KE | ✅ |
| Kiribati | KI | ✅ |
| Korea, Democratic People's Republic of | KP | ❌ |
| Korea, Republic of | KR | ✅ |
| Kuwait | KW | ✅ |
| Kyrgyzstan | KG | ✅ |
| Laos | LA | ❌ |
| Latvia | LV | ✅ |
| Lebanon | LB | ✅ |
| Lesotho | LS | ✅ |
| Liberia | LR | ✅ |
| Libya | LY | ❌ |
| Liechtenstein | LI | ✅ |
| Lithuania | LT | ✅ |
| Luxembourg | LU | ✅ |
| Macao | MO | ✅ |
| Madagascar | MG | ✅ |
| Malawi | MW | ✅ |
| Malaysia | MY | ✅ |
| Maldives | MV | ✅ |
| Mali | ML | ❌ |
| Malta | MT | ✅ |
| Marshall Islands | MH | ✅ |
| Martinique | MQ | ✅ |
| Mauritania | MR | ✅ |
| Mauritius | MU | ✅ |
| Mayotte | YT | ✅ |
| Mexico | MX | ✅ |
| Micronesia | FM | ✅ |
| Moldova | MD | ✅ |
| Monaco | MC | ✅ |
| Mongolia | MN | ✅ |
| Montenegro | ME | ✅ |
| Montserrat | MS | ✅ |
| Morocco | MA | ✅ |
| Mozambique | MZ | ✅ |
| Myanmar | MM | ❌ |
| Namibia | NA | ✅ |
| Nauru | NR | ✅ |
| Nepal | NP | ✅ |
| Netherlands | NL | ✅ |
| New Caledonia | NC | ✅ |
| New Zealand | NZ | ✅ |
| Nicaragua | NI | ✅ |
| Niger | NE | ✅ |
| Nigeria | NG | ✅ |
| Niue | NU | ✅ |
| Norfolk Island | NF | ✅ |
| Northern Mariana Islands | MP | ✅ |
| Norway | NO | ✅ |
| Oman | OM | ✅ |
| Pakistan | PK | ✅ |
| Palau | PW | ✅ |
| Palestine, State of | PS | ✅ |
| Panama | PA | ✅ |
| Papua New Guinea | PG | ✅ |
| Paraguay | PY | ✅ |
| Peru | PE | ✅ |
| Philippines | PH | ✅ |
| Pitcairn | PN | ✅ |
| Poland | PL | ✅ |
| Portugal | PT | ✅ |
| Puerto Rico | PR | ✅ |
| Qatar | QA | ✅ |
| Republic of North Macedonia | MK | ✅ |
| Romania | RO | ✅ |
| Russia | RU | ❌ |
| Rwanda | RW | ✅ |
| Reunion | RE | ✅ |
| Saint Barthélemy | BL | ✅ |
| Saint Helena, Ascension and Tristan da Cunha | SH | ✅ |
| Saint Kitts and Nevis | KN | ✅ |
| Saint Lucia | LC | ✅ |
| Saint Martin (French part) | MF | ✅ |
| Saint Pierre and Miquelon | PM | ✅ |
| Saint Vincent and the Grenadines | VC | ✅ |
| Samoa | WS | ✅ |
| San Marino | SM | ✅ |
| Sao Tome and Principe | ST | ✅ |
| Saudi Arabia | SA | ✅ |
| Senegal | SN | ✅ |
| Serbia | RS | ✅ |
| Seychelles | SC | ✅ |
| Sierra Leone | SL | ✅ |
| Singapore | SG | ✅ |
| Sint Maarten (Dutch part) | SX | ✅ |
| Slovakia | SK | ✅ |
| Slovenia | SI | ✅ |
| Solomon Islands | SB | ✅ |
| Somalia | SO | ❌ |
| South Africa | ZA | ✅ |
| South Georgia and the South Sandwich Islands | GS | ✅ |
| South Sudan | SS | ❌ |
| Spain | ES | ✅ |
| Sri Lanka | LK | ✅ |
| Sudan | SD | ❌ |
| Suriname | SR | ✅ |
| Svalbard and Jan Mayen | SJ | ✅ |
| Sweden | SE | ✅ |
| Switzerland | CH | ✅ |
| Syrian Arab Republic | SY | ❌ |
| Taiwan | TW | ✅ |
| Tajikistan | TJ | ✅ |
| Tanzania, United Republic of | TZ | ✅ |
| Thailand | TH | ✅ |
| Timor-Leste | TL | ✅ |
| Togo | TG | ✅ |
| Tokelau | TK | ✅ |
| Tonga | TO | ✅ |
| Trinidad and Tobago | TT | ✅ |
| Tunisia | TN | ✅ |
| Turkey | TR | ✅ |
| Turkmenistan | TM | ✅ |
| Turks and Caicos Islands | TC | ✅ |
| Tuvalu | TV | ✅ |
| Uganda | UG | ✅ |
| Ukraine | UA | ❌ |
| United Arab Emirates | AE | ✅ |
| United Kingdom | GB | ✅ |
| United States of America | US | ✅ |
| United States Minor Outlying Islands | UM | ✅ |
| Uruguay | UY | ✅ |
| Uzbekistan | UZ | ✅ |
| Vanuatu | VU | ✅ |
| Venezuela | VE | ❌ |
| Vietnam | VN | ✅ |
| Virgin Islands, British | VG | ✅ |
| Virgin Islands, U.S. | VI | ✅ |
| Wallis and Futuna | WF | ✅ |
| Western Sahara | EH | ✅ |
| Yemen | YE | ❌ |
| Zambia | ZM | ✅ |
| Zimbabwe | ZW | ✅ |
| Aland Islands | AX | ✅ |
# Travel rule requirements
Source: https://developers.circle.com/cpn/references/compliance/travel-rule-requirements
Travel rule information is required during payment creation to provide
appropriate regulatory information to the BFI. The following sections outline
the required data that must be securely transmitted to the BFI when a payment is
created. See [JSON Schema](/cpn/concepts/api/json-schema) for more information
on how to validate your API response to the travel rule requirements.
**Note:** Depending on the geography of the payment route, additional travel
rule information may be required to initiate the payment. This additional data
are specified in the data object returned by the [payment creation
endpoint](/api-reference/cpn/cpn-platform/create-payment).
## OFI
| Field | Description |
| ------------------------------------------ | ---------------------- |
| `ORIGINATOR_FINANCIAL_INSTITUTION_NAME` | OFI's business name |
| `ORIGINATOR_FINANCIAL_INSTITUTION_ADDRESS` | OFI's business address |
## Individual
| Field | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ORIGINATOR_NAME` | Sender's full name |
| `ORIGINATOR_ACCOUNT_NUMBER` | Sender's account number reference (originator's wallet address or account number for an OFI virtual or funding account) |
| `ORIGINATOR_ADDRESS` | Sender's address |
| `ORIGINATOR_DATE_OF_BIRTH` | Sender's date of birth |
| `ORIGINATOR_NATIONALITY` | Sender's nationality |
| `ORIGINATOR_NATIONAL_IDENTIFICATION_NUMBER` | Sender's national ID or passport number |
| `BENEFICIARY_NAME` | Recipient's full name |
| `BENEFICIARY_ADDRESS` | Recipient's address |
| `BENEFICIARY_DATE_OF_BIRTH` | Recipient's date of birth |
| `BENEFICIARY_NATIONALITY` | Recipient's nationality |
| `BENEFICIARY_NATIONAL_IDENTIFICATION_NUMBER` | Recipient's national ID or passport number |
## Business
| Field | Description |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ORIGINATOR_NAME` | Sender's legal registered business name |
| `ORIGINATOR_ACCOUNT_NUMBER` | Sender's account number reference (originator's wallet address or account number for an OFI virtual or funding account) |
| `ORIGINATOR_ADDRESS` | Sender's registered place of business |
| `ORIGINATOR_DATE_OF_FORMATION` | Sender's date of company formation |
| `ORIGINATOR_COUNTRY_OF_FORMATION` | Sender's country where the entity was formed |
| `ORIGINATOR_NATIONAL_IDENTIFICATION_NUMBER` | Sender's business registration or tax ID |
| `BENEFICIARY_NAME` | Recipient's legal registered business name |
| `BENEFICIARY_ADDRESS` | Recipient's registered place of business |
| `BENEFICIARY_DATE_OF_FORMATION` | Recipient's date of company formation |
| `BENEFICIARY_COUNTRY_OF_FORMATION` | Recipient's country where the entity was formed |
| `BENEFICIARY_NATIONAL_IDENTIFICATION_NUMBER` | Recipient's business registration or tax ID |
# Validating Brazilian tax ID and bank account numbers
Source: https://developers.circle.com/cpn/references/compliance/validating-brazil-tax-account-id
When providing beneficiary account data during payment creation or during RFIs,
OFIs may need to submit tax IDs or bank account numbers that follow a specific
format. You can often validate these numbers by calculating their check digits.
This topic describes how to checksum various account and tax ID numbers.
## CNPJ
The CNPJ number is a 14 digit number in the following format:
* First 8 digits: base number
* Digits 9-12: branch number
* Last 2 digits: check digits
CNPJ numbers are never all the same digit.
The check digits are calculated in a specific way that you can replicate to
validate the number.
### First check digit
You can calculate the first check digit (digit 13) of a CNPJ number as follows:
1. Multiply the first 12 digits by the following weights, according to their
position: `5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2`
2. Sum the products
3. Calculate the remainder of the sum divided by `11`
4. If the remainder is less than `2`, the check digit is `0`
5. Otherwise, the check digit is `11 - {remainder}`
### Second check digit
You can calculate the second check digit (digit 14) of a CNPJ number as follows:
1. Multiply the first 13 digits by the following weights, according to their
position: `6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2`
2. Sum the products
3. Calculate the remainder of the sum divided by `11`
4. If the remainder is less than `2`, the check digit is `0`
5. Otherwise, the check digit is `11 - {remainder}`
### Example
The following is an example of validating the CNPJ number `11.444.777/0001-61`:
**First digit**
* Weighting and sum:
`((1*5 + 1*4 + 4*3 + 4*2 + 4*9 + 7*8 + 7*7 + 7*6 + 0*5 + 0*4 + 0*3 + 1*2) = 162)`
* Remainder and check digit: `(11 - (184 % 11) = 1)` (matches 13th digit)
**Second digit**
* Weighting and sum:
`((1*6 + 1*5 + 4*4 + 4*3 + 4*2 + 7*9 + 7*8 + 7*7 + 0*6 + 0*5 + 0*4 + 1*3 + 6*2) = 184)`
* Remainder and check digit: `(11 - (184 % 11) = 1)` (matches 14th digit)
## CPF
The CPF number is an 11 digit number that is not all the same digit. The 10th
and 11th digits are check digits. The check digits are calculated in a specific
way that you can replicate to validate the number.
### First check digit
You can calculate the first check digit (digit 10) of a CPF number as follows:
1. Multiply the first 9 digits by the following weights, according to their
position: `10, 9, 8, 7, 6, 5, 4, 3, 2`
2. Sum the products
3. Calculate the remainder of the sum divided by `11`
4. If the remainder is less than `2`, the check digit is `0`
5. Otherwise, the check digit is `11 - {remainder}`
### Second check digit
You can calculate the second check digit (digit 11) of a CPF number as follows:
1. Multiply the first 10 digits by the following weights, according to their
position: `11, 10, 9, 8, 7, 6, 5, 4, 3, 2`
2. Sum the products
3. Calculate the remainder of the sum divided by `11`
4. If the remainder is less than 2, the check digit is `0`
5. Otherwise, the check digit is `11 - {remainder}`
### Example
The following is an example of validating the CPF number `529.982.247-25`:
**First digit**
* Weighting and sum:
`((5*10 + 2*9 + 9*8 + 9*7 + 8*6 + 2*5 + 2*4 + 4*3 + 7*2) = 295)`
* Remainder and check digit: `(11 - (295 % 11) = 2)` (matches 10th digit)
**Second digit**
* Weighting and sum:
`((5*11 + 2*10 + 9*9 + 9*8 + 8*7 + 2*6 + 2*5 + 4*4 + 7*3 + 2*2) = 347)`
* Remainder and check digit: `(11 - (347 % 11) = 5)` (matches 11th digit)
# Validating IBAN bank account numbers
Source: https://developers.circle.com/cpn/references/compliance/validating-iban
Validate IBAN account numbers by checking structure, country-specific length, and bank country match.
When providing beneficiary account data during payment creation or during RFIs,
OFIs may need to submit IBAN (International Bank Account Number) values that
follow a specific format. You can validate these numbers by checking their
structure, their country-specific length, and their country code against the
bank country.
An IBAN has 15 to 34 alphanumeric characters and follows the ISO 13616 standard.
Its structure is:
* First 2 characters: country code (uppercase letters, ISO 3166-1 alpha-2)
* Next 2 characters: check digits (digits)
* Remaining 11 to 30 characters: country-specific BBAN (Basic Bank Account
Number), alphanumeric
IBANs are validated by their format, their country-specific length, and by
matching their country code against `BANK_COUNTRY`.
## Country-specific length
Each IBAN-issuing country has a fixed length defined by the ISO 13616 registry.
The following table lists the expected lengths:
| Country code | Country | Length |
| ------------ | ---------------------- | ------ |
| `AD` | Andorra | 24 |
| `AE` | United Arab Emirates | 23 |
| `AL` | Albania | 28 |
| `AT` | Austria | 20 |
| `AZ` | Azerbaijan | 28 |
| `BA` | Bosnia and Herzegovina | 20 |
| `BE` | Belgium | 16 |
| `BG` | Bulgaria | 22 |
| `BH` | Bahrain | 22 |
| `BI` | Burundi | 27 |
| `BR` | Brazil | 29 |
| `BY` | Belarus | 28 |
| `CH` | Switzerland | 21 |
| `CR` | Costa Rica | 22 |
| `CY` | Cyprus | 28 |
| `CZ` | Czech Republic | 24 |
| `DE` | Germany | 22 |
| `DJ` | Djibouti | 27 |
| `DK` | Denmark | 18 |
| `DO` | Dominican Republic | 28 |
| `EE` | Estonia | 20 |
| `EG` | Egypt | 29 |
| `ES` | Spain | 24 |
| `FI` | Finland | 18 |
| `FK` | Falkland Islands | 18 |
| `FO` | Faroe Islands | 18 |
| `FR` | France | 27 |
| `GB` | United Kingdom | 22 |
| `GE` | Georgia | 22 |
| `GI` | Gibraltar | 23 |
| `GL` | Greenland | 18 |
| `GR` | Greece | 27 |
| `GT` | Guatemala | 28 |
| `HN` | Honduras | 28 |
| `HR` | Croatia | 21 |
| `HU` | Hungary | 28 |
| `IE` | Ireland | 22 |
| `IL` | Israel | 23 |
| `IQ` | Iraq | 23 |
| `IS` | Iceland | 26 |
| `IT` | Italy | 27 |
| `JO` | Jordan | 30 |
| `KW` | Kuwait | 30 |
| `KZ` | Kazakhstan | 20 |
| `LB` | Lebanon | 28 |
| `LC` | Saint Lucia | 32 |
| `LI` | Liechtenstein | 21 |
| `LT` | Lithuania | 20 |
| `LU` | Luxembourg | 20 |
| `LV` | Latvia | 21 |
| `LY` | Libya | 25 |
| `MC` | Monaco | 27 |
| `MD` | Moldova | 24 |
| `ME` | Montenegro | 22 |
| `MK` | North Macedonia | 19 |
| `MN` | Mongolia | 20 |
| `MR` | Mauritania | 27 |
| `MT` | Malta | 31 |
| `MU` | Mauritius | 30 |
| `NI` | Nicaragua | 28 |
| `NL` | Netherlands | 18 |
| `NO` | Norway | 15 |
| `OM` | Oman | 23 |
| `PK` | Pakistan | 24 |
| `PL` | Poland | 28 |
| `PS` | Palestinian Territory | 29 |
| `PT` | Portugal | 25 |
| `QA` | Qatar | 29 |
| `RO` | Romania | 24 |
| `RS` | Serbia | 22 |
| `RU` | Russia | 33 |
| `SA` | Saudi Arabia | 24 |
| `SC` | Seychelles | 31 |
| `SD` | Sudan | 18 |
| `SE` | Sweden | 24 |
| `SI` | Slovenia | 19 |
| `SK` | Slovakia | 24 |
| `SM` | San Marino | 27 |
| `SO` | Somalia | 23 |
| `ST` | São Tomé and Príncipe | 25 |
| `SV` | El Salvador | 28 |
| `TL` | Timor-Leste | 23 |
| `TN` | Tunisia | 24 |
| `TR` | Turkey | 26 |
| `UA` | Ukraine | 29 |
| `VA` | Vatican City | 22 |
| `VG` | British Virgin Islands | 24 |
| `XK` | Kosovo | 20 |
| `YE` | Yemen | 30 |
Some overseas territories use their parent country's IBAN format:
| Territory code | Territory | Parent | Length |
| -------------- | --------------------------- | ------ | ------ |
| `AX` | Åland Islands | `FI` | 18 |
| `BL` | Saint Barthélemy | `FR` | 27 |
| `EA` | Ceuta and Melilla | `ES` | 24 |
| `GF` | French Guiana | `FR` | 27 |
| `GG` | Guernsey | `GB` | 22 |
| `GP` | Guadeloupe | `FR` | 27 |
| `IC` | Canary Islands | `ES` | 24 |
| `IM` | Isle of Man | `GB` | 22 |
| `JE` | Jersey | `GB` | 22 |
| `MF` | Saint Martin (French part) | `FR` | 27 |
| `MQ` | Martinique | `FR` | 27 |
| `NC` | New Caledonia | `FR` | 27 |
| `PF` | French Polynesia | `FR` | 27 |
| `PM` | Saint Pierre et Miquelon | `FR` | 27 |
| `RE` | Réunion | `FR` | 27 |
| `SJ` | Svalbard and Jan Mayen | `NO` | 15 |
| `TF` | French Southern Territories | `FR` | 27 |
| `WF` | Wallis and Futuna Islands | `FR` | 27 |
| `YT` | Mayotte | `FR` | 27 |
## Cross-field validation with bank country
When both IBAN and `BANK_COUNTRY` are provided, the IBAN's country code (first 2
characters) must match `BANK_COUNTRY`. Overseas territories resolve to their
parent country before the check. For example, an IBAN starting with `GG`
(Guernsey) is treated as `GB`.
## Example
The following is an example of validating the IBAN `GB82WEST12345698765432`:
**Structure**
* Country code: `GB` (United Kingdom)
* Check digits: `82`
* BBAN: `WEST12345698765432` (18 characters)
* Total length: `22` (matches the fixed length for `GB`)
**Cross-field check**
* IBAN country code: `GB`
* `BANK_COUNTRY`: `GB` (matches)
# Payment and transaction failure reasons
Source: https://developers.circle.com/cpn/references/errors/payment-and-transaction-failure-reasons
When payments or transactions fail, you'll receive an object containing the
failure reason. Depending on when the failure occurs, this information is
returned in the synchronous (API) or asynchronous (webhook) response. The
following sections outline the reasons that each of these components might fail.
## Payments
When a payment fails in CPN there are no further actions you can take with that
payment ID. You should restart the payment workflow by requesting a new quote
and accepting it to create a new payment.
The payment failure notification includes a failure reason (listed below) and a
[failure code](/cpn/references/errors/payment-failure-codes) containing more
specific information about the failure. The failure code provides information
about how to resolve the issue and create a new payment.
| Failure reason | Description |
| ----------------------------------------- | ----------------------------------------------------------------------------- |
| `TRAVEL_RULE_FAILED` | The travel rule information included with the payment was rejected |
| `BANK_VERIFICATION_FAILED` | The bank information included with the payment was rejected |
| `RFI_VERIFICATION_FAILED` | The payment failed due to issues related to RFI handling or verification |
| `EXISTING_RFI_PENDING` | An RFI exists on the customer in a non-terminal state |
| `ONCHAIN_SETTLEMENT_FAILED` | The payment couldn't be processed due to an issue with the onchain settlement |
| `ONCHAIN_SETTLEMENT_CUTOFF_TIME_EXCEEDED` | The signed transaction was submitted after the onchain settlement cutoff |
| `FIAT_SETTLEMENT_FAILED` | The payment couldn't be completed due to routing or bank settlement issues |
| `COMPLIANCE_CHECK_FAILED` | The payment was rejected due to a compliance check failure |
| `CANCELLED` | The payment is canceled by the originator |
| `PAYMENT_EXPIRED` | The payment expired before a signed transaction was submitted |
| `OTHER` | General payment failure not covered by other reasons |
## Transactions
If an onchain transaction fails or becomes stuck, you can create a new
transaction or attempt to replace or accelerate the transaction. You may
continue to troubleshoot onchain transactions until the payment expires. Note
that only one transaction can be associated with a given payment, so you must
wait until the current transaction fails or attempt to replace it if you need to
update the transaction.
| Failure reason | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CPN_PAYMENT_EXPIRED` | The payment corresponding to this transaction is expired.
**Resolution**: Create a new payment and ensure the onchain transaction is completed in its valid time frame. |
| `SIGNED_TRANSACTION_EXPIRED` | (Solana only) The signed transaction is expired. Solana requires you to submit a signed transaction in 150 blocks (\~1 min).
**Resolution:** Initiate a new transaction and submit it in the appropriate time frame. |
| `NONCE_TOO_LOW` | The nonce of the signed transaction is lower than the current wallet nonce.
**Resolution:** Submit a new transaction using the latest nonce value for the wallet. |
| `INSUFFICIENT_GAS_BALANCE` | Not enough native tokens were available in the wallet to cover the gas fee for the transaction.
**Resolution:** Fund the wallet with the appropriate amount of native tokens and initiate a new transaction. |
| `GAS_PRICE_TOO_LOW` | The specified gas fee is too low, which may prevent the transaction from being included in a block.
**Resolution:** Create a new transaction with a higher gas fee. |
| `INSUFFICIENT_TOKEN_BALANCE` | The wallet does not have enough USDC in it for the transfer amount.
**Resolution:** Fund the wallet with the appropriate amount of USDC for the payment, then initiate a new transaction. |
| `OUT_OF_GAS` | For EVM chains, the gas limit for the signed transaction is insufficient to cover the execution cost. For Solana, the allocated compute budget falls short of the transaction's requirements, preventing successful execution.
**Resolution:** Create a new transaction with a higher gas limit. |
| `TX_REPLACEMENT_FAILED` | The transaction replacement failed because another transaction with the same nonce (and higher fee) is already the mempool or was mined first.
If the replacement fails, the original transaction submitted to the blockchain is still executed. |
| `SOL_TX_ALREADY_IN_CACHE` | (Solana only) The transaction has already been broadcast and is present in the network's cache.
**Resolution:** Wait for the current transaction's confirmation or failure before proceeding. |
| `TX_ALREADY_CONFIRMED` | The transaction was confirmed onchain before the current broadcast attempt.
**Resolution:** If this was unexpected, open a support ticket for further investigation. |
| `FAILED_ONCHAIN` | The transaction failed execution on the blockchain.
**Resolution:** Review the onchain error, correct any issues, and submit a new transaction. |
| `SOL_BLOCKHASH_EXPIRED` | (Solana only) The Solana blockhash assigned to the transaction has expired.
**Resolution:** Initiate a new transaction. |
| `TRANSACTION_EXPIRED` | The transaction is expired and you aren't able to perform any further actions.
**Resolution:** Initiate a new transaction. |
# Payment failure codes
Source: https://developers.circle.com/cpn/references/errors/payment-failure-codes
When a payment fails, CPN provides a
[failure reason](/cpn/references/errors/payment-and-transaction-failure-reasons)
and a failure code. Depending on when the failure occurs, this information is
returned in the synchronous (API) or asynchronous (webhook) response. This
failure code can help you understand why the payment failed, and take
appropriate steps to resolve the issue. The following sections outline the
specific failure codes for failed payments.
## General
These codes apply when the failure reason is `OTHER`.
| Failure code | Description |
| ------------ | ------------- |
| `PM00001` | General error |
## Travel rule
These codes apply when the failure reason is `TRAVEL_RULE_FAILED`.
| Failure code | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------- |
| `PM01000` | A general travel rule failure occurred during travel rule verification |
| `PM01001` | The originator address is missing data or doesn't meet required formatting or jurisdictional standards |
| `PM01002` | The beneficiary address is missing data or doesn't meet required formatting or jurisdictional standards |
| `PM01003` | The national ID or passport number provided for the originator is invalid, missing, or fails verification |
| `PM01004` | The national ID or passport number provided for the beneficiary is invalid, missing, or fails verification |
## Bank verification
These codes apply when the failure reason is `BANK_VERIFICATION_FAILED`.
| Failure code | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PM02000` | General bank detail validation failure |
| `PM02001` | The beneficiary bank account details (SWIFT BIC, account alias, account number) are invalid, malformed, or not recognized by the bank or network |
| `PM02002` | The beneficiary bank account details provided don't match the registered beneficiary in the bank's records |
| `PM02003` | The receiving bank isn't supported |
| `PM02004` | The specified account type (checking, savings) isn't supported |
| `PM02005` | The beneficiary bank or payment participant is unavailable or offline |
## RFI verification
These codes apply when the failure reason is `RFI_VERIFICATION_FAILED`.
| Failure code | Description |
| ------------ | ---------------------------------------------------- |
| `PM03000` | General RFI verification failure |
| `PM03001` | Missing or improperly formatted RFI documents |
| `PM03002` | Conflicting data in the RFI submission |
| `PM03003` | Expired or outdated RFI documents |
| `PM03004` | Rejected after manual compliance review |
| `PM03005` | RFI response or review not completed in allowed time |
## Existing RFI pending
These codes apply when the failure reason is `EXISTING_RFI_PENDING`.
| Failure code | Description |
| ------------ | ----------------------------------------------------- |
| `PM04000` | An RFI exists on the customer in a non-terminal state |
## Onchain settlement
These codes apply when the failure reason is `ONCHAIN_SETTLEMENT_FAILED`.
| Failure code | Description |
| ------------ | ------------------------------------------------------------------------------- |
| `PM05000` | General onchain settlement failure |
| `PM05001` | Received funds are invalid (wrong asset, insufficient amount, wrong blockchain) |
## Fiat settlement
These codes apply when the failure reason is `FIAT_SETTLEMENT_FAILED`.
| Failure code | Description |
| ------------ | -------------------------------------------------------------- |
| `PM06000` | General fiat settlement or payout failure |
| `PM06001` | Rejected by the receiving bank |
| `PM06002` | Rejected by the sending bank |
| `PM06003` | Destination account invalid or unregistered |
| `PM06004` | Destination account blocked |
| `PM06005` | Transaction amount exceeds permitted limit |
| `PM06006` | Beneficiary bank or payment participant unavailable or offline |
| `PM06007` | Currency unsupported or invalid |
## Compliance check
These codes apply when the failure reason is `COMPLIANCE_CHECK_FAILED`.
| Failure code | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| `PM07000` | General compliance check failure |
| `PM07001` | Originator blocked or ineligible |
| `PM07002` | Beneficiary blocked or ineligible |
| `PM07003` | OFI compliance check failed.
You may need to complete KYB before proceeding with further payments |
| `PM07004` | The beneficiary bank account failed compliance checks |
## Canceled
These codes apply when the failure reason is `CANCELLED`.
| Failure code | Description |
| ------------ | ------------------------------------- |
| `PM08000` | Funds were canceled by the originator |
## Payment expired
These codes apply when the failure reason is `PAYMENT_EXPIRED`.
| Failure code | Description |
| ------------ | ----------------------------------------------------- |
| `PM09000` | Crypto funds not received in the expected time window |
## Onchain settlement cutoff
These codes apply when the failure reason is
`ONCHAIN_SETTLEMENT_CUTOFF_TIME_EXCEEDED`.
| Failure code | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PM09001` | The signed transaction was submitted after the onchain settlement cutoff. The cutoff time is the payment's expiry time minus a blockchain-specific buffer. |
# Payment reason codes
Source: https://developers.circle.com/cpn/references/errors/payment-reason-codes
When you
[create a payment with the CPN API](/api-reference/cpn/cpn-platform/create-payment),
you should include a `reasonForPayment` code in the API request. Each code
provides a specific payment reason.
The following table outlines each code and the reason it represents.
| Reason code | Description |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| `PMT001` | Invoice payment |
| `PMT002` | Payment for services |
| `PMT003` | Payment for software |
| `PMT004` | Payment for imported goods |
| `PMT005` | Travel services |
| `PMT006` | Transfer to own account |
| `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 payment methods
Source: https://developers.circle.com/cpn/references/payments/supported-payment-methods
CPN supports multiple payment methods for transferring funds to recipients in
different countries. The available payment methods depend on the destination
country and are determined by the payment rails available in that region.
You can discover available payment methods for specific routes using the
[configurations overview endpoint](/api-reference/cpn/cpn-platform/get-payment-configurations-overview)
or the [list routes endpoint](/api-reference/cpn/cpn-platform/list-routes).
**Note:** Payment method availability may vary based on your account
configuration and the specific payment route. Always use the [list routes
endpoint](/api-reference/cpn/cpn-platform/list-routes) to verify available
payment methods for your specific use case.
## Settlement times
Payment methods have different settlement characteristics:
* **Instant payments** (for example `PIX`, `SPEI`, `IMPS`): Typically settle in
seconds to minutes
* **Batched payments** (for example `WIRE`, `CHATS`): May take 1-2 business days
depending on the route
Actual settlement times are provided in the quote response via the
`fiatSettlementTime` field.
## Payment methods by region
The following sections detail payment methods available for specific destination
countries and regions.
### Argentina
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Brazil
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | --------------------------------------------------------------------- | --------------------------- |
| `PIX` | BRL | Brazil's instant payment system that enables real-time transfers 24/7 | 5 minutes |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Chile
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### China
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ----------------------------------------------------------------------------- | --------------------------- |
| `CIPS` | CNY | China's Cross-Border Interbank Payment System for international RMB transfers | 1-2 business days |
| `WIRE` | CNY, USD | Wire transfer for international payments | 1-2 business days |
### Colombia
| Payment Method | Currency | Description | Approximate Processing Time |
| --------------- | -------- | ---------------------------------------------------------------------------------- | --------------------------- |
| `BANK-TRANSFER` | COP | Bank transfer for domestic payments in Colombia, processed through the ACH network | 1-2 business days |
| `NEQUI` | COP | Colombia's digital wallet and instant payment system | 5 minutes |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### European Union
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | -------------------------------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments to European countries | 1-2 business days |
**Note:** SEPA payments will attempt instant SEPA first, which delivers in
minutes. If instant SEPA is not available for the transaction, it will fall
back to regular SEPA, which takes 1 business day.
**Note:** WIRE is available for the following countries: Andorra, Austria,
Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland,
France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Liechtenstein,
Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania,
Slovakia, Slovenia, Spain, Sweden, Switzerland, and United Kingdom.
### Hong Kong
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------------------------------------------- | --------------------------- |
| `CHATS` | HKD, USD | Hong Kong's Clearing House Automated Transfer System for interbank transfers | Same day |
| `FPS` | HKD | Hong Kong's Faster Payment System for real-time payments | 5 minutes |
| `WIRE` | HKD, USD | Wire transfer for international payments | 1-2 business days |
### India
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ------------------------------------------------------------------------ | --------------------------- |
| `IMPS` | INR | India's Immediate Payment Service for instant interbank transfers | 5 minutes |
| `NEFT` | INR | India's National Electronic Funds Transfer for scheduled batch transfers | Same day |
| `RTGS` | INR | India's Real Time Gross Settlement for high-value transfers | 5 minutes |
### Japan
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Kenya
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Mexico
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ------------------------------------------------------------------------ | --------------------------- |
| `SPEI` | MXN | Mexico's electronic interbank payment system operated by Banco de México | 5 minutes |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Nigeria
| Payment Method | Currency | Description | Approximate Processing Time |
| --------------- | -------- | --------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `BANK-TRANSFER` | NGN | Nigeria's NIBSS (Nigeria Inter-Bank Settlement System) instant payment system for real-time interbank transfers | 5 minutes |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Singapore
| Payment Method | Currency | Description | Approximate Processing Time |
| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `BANK-TRANSFER` | SGD | Bank transfer for domestic payments in Singapore, may be processed through FAST (Fast And Secure Transfers) or MEPS RTGS (Real-Time Gross Settlement) | 30 minutes |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### South Africa
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### South Korea
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### Taiwan
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ---------------------------------------- | --------------------------- |
| `WIRE` | USD | Wire transfer for international payments | 1-2 business days |
### United States
| Payment Method | Currency | Description | Approximate Processing Time |
| -------------- | -------- | ----------------------------------------------------------------- | --------------------------- |
| `FEDWIRE` | USD | US Federal Reserve's wire transfer system for same-day settlement | Same day |
## Payment method characteristics
### Limits
Each payment method has minimum and maximum transfer limits that vary by:
* Destination country
* Destination currency
* Payment method type
Use the [list routes endpoint](/api-reference/cpn/cpn-platform/list-routes) to
retrieve specific limits for your payment route. When using V2 transactions,
pass `transactionVersion=VERSION_2` so returned crypto min limits include a
chain-specific buffer for fees (for example, gas fee), paid in USDC from the
source amount.
# Magic values for testing
Source: https://developers.circle.com/cpn/references/testing/magic-values
You can use specific values when you create a payment on testnet to get the CPN
API to return certain statuses. These magic values can be useful to test how
your integration handles specific states and situations of payments. Magic
values can be used in the sandbox environment.
The following sections outline the magic values you can pass to the sandbox API
and the effect that they have on the response from the API.
**Note:** All of the magic values listed in the following sections are case
sensitive. The specified field must be set to the exact value in the table in
order for them to work.
## Create payment
You can test payments by providing magic values on the `ORIGINATOR_NAME` field
when making requests to the
[create a payment endpoint](/api-reference/cpn/cpn-platform/create-payment). The
following table outlines the magic values and their effects on the payment
response:
| Magic value | Response |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Failed` | Synchronous payment failure. Payment is returned with the `FAILED` status in the synchronous response. |
| `AsyncFailed` | Asynchronous payment failure. Payment is returned with the `CREATED` status in the synchronous response, followed by a webhook that updates the payment to the `FAILED` status. |
| `AsyncSuccess` | Asynchronous payment success. Payment is returned with the `CREATED` status in the synchronous response, followed by a webhook that updates the payment to the `CRYPTO_FUNDS_PENDING` status. |
| `CreateRfi` | Payment is returned with an active level 1 RFI in the synchronous response. |
| `CreateRfiL2` | Payment is returned with an active level 2 RFI in the synchronous response. |
| `CreateRfiL3` | Payment is returned with an active level 3 RFI in the synchronous response. |
| `AsyncRfi` | Payment is returned with the `CREATED` status in the synchronous response, followed by a webhook that creates an RFI on the payment. |
| `Delayed` | Payment is returned with the `CRYPTO_FUNDS_PENDING` status in the synchronous response. After an onchain transaction is received and after the `FIAT_PAYMENT_INITIATED` notification occurs, the fiat settlement time on the payment increases and a payment delayed webhook notifications is sent. The `COMPLETED` webhook is delayed by a few seconds. |
| `Expired` | Payment is returned with the `CRYPTO_FUNDS_PENDING` status in the synchronous response. After a few seconds the payment status is updated to `FAILED` with the reason `ONCHAIN_SETTLEMENT_CUTOFF_TIME_EXCEEDED`. |
| `FailThenRefundWithCompleted` | Payment is returned with the `CRYPTO_FUNDS_PENDING` status in the synchronous response. After an onchain transaction is received and after the `FIAT_PAYMENT_INITATED` notification occurs, the payment is moved to a `FAILED` state and a refund with the `COMPLETED` status is added to the payment.
Using this magic value just fails the payment and sends the refund notification. It does not send the funds from the onchain transaction back. The `txHash` returned in the sandbox environment is a randomly generated transaction hash. |
| `FailThenRefundCreatedThenFailed` | Payment is returned with the `CRYPTO_FUNDS_PENDING` status in the synchronous response. After an onchain transaction is received, a `FIAT_PAYMENT_INITIATED` notification occurs, and the payment is moved to a `FAILED` state. A refund with the `CREATED` status is added to the payment, followed by a webhook that updates the refund to the `FAILED` state.
This magic value simulates a scenario where the payment fails and an attempt to issue a refund is made, but the refund also fails, for example due to a downstream system error. |
| `FailThenRefundCreatedThenCompleted` | Payment is returned with the `CRYPTO_FUNDS_PENDING` status in the synchronous response. After an onchain transaction is received, a `FIAT_PAYMENT_INITIATED` notification occurs, and the payment is moved to a `FAILED` state. A refund with the `CREATED` status is added to the payment, followed by a webhook that updates the refund to the `COMPLETED` state.
This value simulates a successful refund process after a failed payment. The original onchain funds are still not returned to the sender. The `txHash` used for testnet is randomly generated. |
## Submit RFI
You can test RFIs by providing magic values on the `NAME` field when making
requests to the [submit RFI data](/api-reference/cpn/cpn-platform/submit-rfi)
endpoint. The following table outlines the magic values and their effects on the
RFI response:
| Magic value | Response |
| ----------- | ----------------------------------------------- |
| `InReview` | Stuck RFI. RFI stays in the `IN_REVIEW` status. |
| `Rejected` | RFI is rejected. |
**Note:** Any value in the `NAME` field besides the values in the table
results in an approved RFI.
# Webhook events
Source: https://developers.circle.com/cpn/references/webhooks/webhook-events
Reference for CPN webhooks for OFIs: payment, RFI, transaction, and refund notification types and payloads.
Components of CPN send webhook notifications when events occur, typically for a
change in state. These webhook notifications are sent to the OFI so that they
can track and manage payments in CPN. The webhooks described in this topic are
specific to OFIs only.
All webhook notifications follow the same payload structure. The
`notificationType` field indicates the event type, and the `notification` field
contains the event-specific data object. The `notification` object schema
matches the `data` field returned by the corresponding API endpoint (see the
[API reference](/api-reference/cpn/cpn-platform/) for details).
```json JSON theme={null}
{
"subscriptionId": "cd198ca6-9fc4-41d6-b6b1-8e2a9c975352",
"notificationId": "0761b030-0c75-472a-b471-74d90a7e796c",
// Event type, determines the structure of the notification object
"notificationType": "cpn.payment.cryptoFundsPending",
// Event-specific data object, structure varies by notificationType
// See corresponding section below for the object schema (Payment, RFI, Transaction, or Refund)
"notification": {
// Object matching the event type
},
"timestamp": "2024-09-30T17:48:05.420577158Z",
"version": 2
}
```
The following sections describe the webhook events by component.
## Source IP addresses
CPN webhook notifications are delivered from a static set of source IP
addresses. Allowlist these addresses at your firewall, load balancer, or cloud
security group as an additional layer of defense alongside
[signature verification](/api-reference/verify-webhook-signatures). The same IPs
apply to both mainnet and testnet:
* `35.169.154.32`
* `3.90.127.28`
* `3.230.111.7`
* `54.88.227.75`
For endpoint setup details, see
[Set up a webhook endpoint](/api-reference/webhook-endpoints).
## Payment events
The `notification` object has the same schema as the `data` field returned by
[Get Payment](/api-reference/cpn/cpn-platform/get-payment).
| Event name | Description | Trigger when |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cpn.payment.cryptoFundsPending` | The payment has been created and is waiting for funds to be deposited | The payment is ready to accept crypto funds |
| `cpn.payment.fiatPaymentInitiated` | The BFI has initiated the fiat payment to the receiver's bank account | The onchain transaction is confirmed by the BFI and the BFI initiated the fiat payment |
| `cpn.payment.completed` | The payment is fully processed | The fiat payment has been sent.
Depending on the payment method, the receiver may or may not have received the transfer. At this point, the CPN payment is complete. |
| `cpn.payment.failed` | The payment failed due to errors in processing, insufficient funds, or compliance rejection | The payment can't proceed due to an issue |
| `cpn.payment.delayed` | There is a delay to the estimate of payment settlement time. No status has changed but an updated settlement time is included in the payload | The BFI has an internal issue such that fiat settlement takes longer to process than originally stated |
| `cpn.payment.inManualReview` | A manual review has been placed on the payment. The payment status itself remains `CREATED`. | A payment is created but placed under manual review |
## RFI events
The `notification` object has the same schema as the `data` field returned by
[Get RFI](/api-reference/cpn/cpn-platform/get-rfi).
| Event name | Description | Trigger when |
| ----------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `cpn.rfi.informationRequired` | Additional compliance information is needed before the payment can proceed | The BFI requests more information to proceed with the payment |
| `cpn.rfi.inReview` | The submitted RFI response is under review by the BFI | The OFI submits the required information through the API endpoint |
| `cpn.rfi.approved` | The RFI has been reviewed and approved, the payment can proceed | The BFI approves the submitted information |
| `cpn.rfi.rejected` | The RFI has been reviewed and rejected, the payment is failed | The BFI rejects the submitted information |
## Transaction events
The `notification` object has the same schema as the `data` field returned by
[Get Transaction](/api-reference/cpn/cpn-platform/get-transaction).
| Event name | Description | Trigger when |
| ----------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| `cpn.transaction.broadcasted` | The signed transaction is sent to the blockchain for confirmation | The signed transaction has been broadcast |
| `cpn.transaction.completed` | The transaction was successfully included in a block | The blockchain confirms the transaction |
| `cpn.transaction.failed` | The transaction failed due to issues such as incorrect parameters or insufficient gas fees | The blockchain failed to confirm the transaction |
## Refund events
The `notification` object has the same schema as the `data` field returned by
[Get Refund](/api-reference/cpn/cpn-platform/get-refund).
| Event name | Description | Trigger when |
| ---------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------- |
| `cpn.refund.created` | The BFI created a refund for the associated payment | The BFI initiated the refund.
**Note:** Not all BFIs send this event. |
| `cpn.refund.failed` | The refund could not be processed | The BFI encountered an error processing the refund |
| `cpn.refund.completed` | The refund is complete | The BFI confirms that the refund was successfully returned to the refund address |
## Webhooks from other Circle products
The event types in the previous sections are delivered through **CPN**
subscriptions. When you also use **Programmable Wallets** or **Circle APIs** for
liquidity and treasury, you may configure additional webhook subscriptions for
those products. Payload shape and `notificationType` values differ from CPN
webhooks.
| Product area | Subscription endpoint | Reference |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| CPN payments | [Create subscription](/api-reference/cpn/common/create-subscription) (`POST /v2/cpn/notifications/subscriptions`) | This page and [Set up a webhook endpoint](/api-reference/webhook-endpoints) |
| Programmable Wallets | [Create subscription](/api-reference/wallets/common/create-subscription) (`POST /v2/notifications/subscriptions`) | [Inbound transaction](/api-reference/wallets/common/transactions-inbound) and [outbound transaction](/api-reference/wallets/common/transactions-outbound) webhook events |
| Circle APIs (liquidity) | [Create subscription](/api-reference/circle-mint/general/create-subscription) (`POST /v1/notifications/subscriptions`) | [Notifications data models](/circle-mint/references/webhook-notifications) |
CPN and other Circle products each enforce their own API keys and notification
contracts. Plan separate subscriber endpoints or routing logic if you need all
event types in one service.
# Stablecoin payments
Source: https://developers.circle.com/cpn/stablecoin-payments
Accept and send USDC and EURC onchain with external counterparties through Circle Payments Network, using Stablecoin Payins and Stablecoin Payouts.
Stablecoin Payments lets you move USDC and EURC onchain with external
counterparties as a self-managed product in
[Circle Payments Network (CPN)](/cpn). You hold the stablecoins and operate the
flow; Circle provides the APIs, deposit addresses, recipient screening, and
onchain settlement. The product covers two flows:
Accept USDC or EURC from third parties. Create a payment intent, share the
deposit address Circle assigns, and reconcile transfers as they settle.
Send USDC or EURC to third-party wallets you register and approve through
the Address Book, with Travel Rule support.
Stablecoin Payins and Stablecoin Payouts require explicit activation on your
Circle account. To request access, contact Circle through the
[Circle Mint contact form](https://www.circle.com/mint-contact).
## Key features
* **Payment intents:** Create continuous deposit addresses for recurring payins,
or transient addresses scoped to a single checkout.
* **Address Book:** Register, screen, and reuse third-party recipient addresses
before you send payouts.
* **Refunds:** Return funds against a settled payin during the refund window.
* **Travel Rule support:** Attach originator and beneficiary data to third-party
payouts where required.
* **Webhooks:** Subscribe to intent, payment, recipient, and payout events to
track activity. See
[Webhook events](/cpn/stablecoin-payments/references/webhook-events).
## API summary
| Product | Area | Purpose |
| ------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------- |
| Stablecoin Payins | [Payment Intents](/api-reference/circle-mint/payments/create-payment-intent) | Create and manage deposit-address intents. |
| Stablecoin Payins | [Payments](/api-reference/circle-mint/payments/list-payments) | Track settled inbound transfers and refunds. |
| Stablecoin Payouts | [Payouts](/api-reference/circle-mint/payouts/create-payout) | Send USDC or EURC to registered recipients. |
| Stablecoin Payouts | [Address Book](/api-reference/circle-mint/payouts/create-address-book-recipient) | Register, screen, and manage recipient addresses. |
## Get started
Create a payment intent, share the deposit address, and confirm the payment
when it settles.
Register a recipient in the Address Book and send USDC or EURC onchain to a
third party.
Understand payment intents, the payments resource, recipient lifecycle, and
refunds.
# How stablecoin payments work
Source: https://developers.circle.com/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work
Understand how Stablecoin Payins and Stablecoin Payouts work in Circle Payments Network, including payment intents, recipient management, refunds, and regional availability.
Stablecoin Payments in Circle Payments Network (CPN) covers two products for
moving USDC and EURC onchain with external counterparties: Stablecoin Payins and
Stablecoin Payouts. Stablecoin Payins accepts USDC or EURC sent from an external
wallet to a deposit address that Circle assigns to your account. Stablecoin
Payouts sends USDC or EURC from your Circle balance to a third-party recipient
at a wallet address you register.
Stablecoin Payins and Stablecoin Payouts require explicit activation on your
Circle account, separate from base Circle Mint access. To request access,
contact Circle through the
[Circle Mint contact form](https://www.circle.com/mint-contact).
## Payment intents
A payment intent is a first-class API object that represents a single agreement
to receive funds onchain. The intent bundles the settlement currency, the
allowed blockchains, and a deposit address that Circle assigns asynchronously
after creation. Your server creates the intent, displays the address to the
payer once Circle returns it, and observes settlement events as inbound
transfers arrive.
Each transfer that settles against an intent produces a `payments` resource.
Payments and refunds share that single resource, discriminated by `type`, so
your integration listens to the same notification channel for both inbound and
outbound activity tied to an intent.
The intent's lifecycle depends on its mode. A transient intent has a target
amount, so it moves through `created`, `pending`, `complete`, `expired`,
`failed`, and `refunded` states; when it reaches `complete`, the latest timeline
entry carries a context of `paid`, `underpaid`, or `overpaid`. A continuous
intent has no target amount, so it stays at `active` after Circle assigns the
deposit address and never advances to `complete` on its own. To reconcile
settled transfers against a continuous intent, listen to the `payments` webhook
or call `GET /v1/payments?paymentIntentId={id}`; the intent's own `paymentIds`
array is only populated for transient intents.
### Continuous and transient modes
A payment intent operates in one of two modes. Continuous mode produces a
long-lived deposit address that can receive multiple transfers over time, which
suits recurring billing, treasury top-ups, or any flow where a payer needs to
send funds more than once. Transient mode produces an address scoped to a single
checkout: you declare the expected amount up front, and the address is not
intended for reuse after that one payment settles. Continuous is the default
behavior.
The following table compares what you send when creating each type of intent and
how the resulting deposit address behaves.
| | Continuous | Transient |
| ------------------ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Amount at creation | Omitted. You set `currency` and settlement. | You set a fixed `amount`. |
| Deposit address | Same address can receive multiple transfers. | Tied to one checkout; not reused after settlement. |
| How you select it | Omit `type` (default) or set `type` to `continuous`. | Set `type` to `transient`. |
| Intent lifecycle | Stays at `active`. Never advances to `complete` on its own. | Advances to `complete` after a transfer settles, with context `paid`/`underpaid`/`overpaid`. |
| `paymentIds` | Not populated on the intent. Use the `payments` webhook or `GET /v1/payments?paymentIntentId={id}`. | Populated when the intent reaches `complete`. |
For continuous intents, every inbound transfer is recorded as its own payment
against the same intent. For transient intents, the single deposit address is
tied to one checkout or invoice and should not be reused after the payment
settles.
## Payin flow
A stablecoin payin moves through four logical stages: the customer signals
intent to pay, Circle assigns a deposit address, the customer transfers funds
onchain, and Circle notifies your server when the payment settles. The following
diagram illustrates the actors involved and the order of operations.
```mermaid theme={null}
sequenceDiagram
participant C as Customer
participant W as Customer-facing UI
participant S as Your server
participant M as Circle APIs
Note over C,M: Create payment intent
C->>W: Chooses to pay with stablecoins on a blockchain
W->>S: Requests onchain deposit
S->>M: Sends payment intent creation request
M-->>S: Sends payment intent
Note over C,M: Send and display deposit address
M-->>S: Sends notification containing: payment intent status (pending), deposit address
S->>W: Sends deposit address
W->>C: Displays deposit address
Note over C,M: Customer pays onchain and Circle notifies your server
C->>M: Transfers funds to deposit address
M-->>S: Sends notification containing: payment status (paid)
M-->>S: Sends notification (transient intents only): payment intent status (complete)
Note over C,M: Confirm payment to customer
S->>W: Sends payment confirmation
W->>C: Displays payment confirmation
```
For continuous intents, only the `payments` notification fires; the intent
itself does not transition to `complete`. Use the `payments` webhook (or list
payments with `GET /v1/payments?paymentIntentId={id}`) to reconcile settled
transfers against a continuous intent.
## Payout flow
A stablecoin payout moves from your Circle balance to a registered recipient
address in three logical stages: you register the recipient, you create the
payout, and Circle delivers funds onchain. The following diagram shows the
actors and order of operations.
```mermaid theme={null}
sequenceDiagram
participant S as Your server
participant M as Circle APIs
participant R as Recipient wallet
Note over S,M: Create address book recipient
S->>M: Sends create address book recipient request
M-->>S: Sends address book recipient
Note over S,M: Create payout
S->>M: Sends create payout request
M-->>S: Sends payout (pending)
Note over M,R: Onchain delivery
M->>R: Transfers USDC or EURC to registered address
Note over S,M: Payout complete
M-->>S: Sends notification containing: payout status (complete)
```
## First-party versus third-party transfers
Stablecoin Payouts is always a third-party flow: the destination wallet belongs
to a merchant, vendor, contractor, customer, or other counterparty outside your
business. If you need to send USDC or EURC to a wallet that your own business
controls, such as a treasury wallet or a self-custody address, use the Core API
instead. The following table summarizes when each path applies.
| Use Stablecoin Payouts when… | Use the Core API when… |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| You're paying a third party, such as a vendor, contractor, marketplace seller, remittance recipient, or end customer. The address belongs to someone other than your business and requires recipient review. | You're moving funds to a wallet your business owns or controls, such as a treasury wallet or self-custody address. See [Transfer USDC Onchain](/circle-mint/howtos/transfer-on-chain). |
The two paths use different APIs and have different compliance treatments;
choosing the right one matters for both operational behavior and Travel Rule
applicability.
## Address book and recipient lifecycle
Every Stablecoin Payouts destination must be registered through the Address Book
API before you can send funds to it. Registration is per recipient, not per
payout, so once a recipient is active you can send additional payouts to the
same address without re-registering.
A recipient moves through the following lifecycle:
* `pending`: Circle is reviewing the recipient.
* `inactive`: The recipient is in the delayed withdrawals holding period. This
state appears for 24 hours when the **delayed withdrawals** toggle is on in
the Circle Mint console; otherwise recipients skip this state.
* `active`: The recipient is ready to receive payouts.
* `denied`: The recipient failed review and cannot receive payouts.
Procedural details, including the API request to register a recipient, live in
the
[Send a stablecoin payout](/cpn/stablecoin-payments/howtos/send-stablecoin-payout)
how-to.
## Refunds
Refunds let you return funds against a payment intent that has already received
at least one settled payment. The following constraints define the refund
window:
* You can start a refund only after at least one payment on the intent has
completed and settled. You cannot refund a pending payment.
* Refunds are available for 30 days from the intent's creation date. In that
window you can issue more than one partial refund, up to your Circle account
limits.
* Once a refund is initiated, the payment intent transitions to `refunded` and
stops accepting new payins. Treat the intent as terminal at that point and
create a new intent for any future checkout.
For the refund procedure, see
[Refund a stablecoin payin](/cpn/stablecoin-payments/howtos/refund-stablecoin-payin).
## Blockchain support
Stablecoin Payins and Stablecoin Payouts support USDC and EURC on a defined set
of blockchains. For the authoritative list of supported blockchains and
currencies, see
[Supported blockchains](/cpn/stablecoin-payments/references/supported-blockchains).
### Memo-based blockchains
Some blockchains identify the destination with a memo or tag in addition to the
address. For Stellar (`XLM`) and Hedera (`HBAR`), a payment intent's deposit
address includes an `addressTag`. If the payer's wallet enforces memos, they
must include this tag when they send funds.
For XRPL (`XRP`) payout destinations, Stablecoin Payouts supports only the
classic-address format, such as `rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY`. The
x-address format is not supported. Use the `addressTag` field to specify the
destination tag.
## Regional availability
Stablecoin Payouts is available through Circle's U.S. entity (Circle LLC),
Circle's Singapore entity (`CIRCLE_SG`), and Circle's France entity (Circle SAS,
`CIRCLE_FR`).
Payouts booked through Circle Singapore or Circle SAS have additional Travel
Rule data requirements described in
[Travel rule compliance](/circle-mint/references/travel-rule-compliance).
Payouts booked through Circle SAS fall under the EU Transfer of Funds Regulation
and can include an optional legal entity identifier (LEI) on beneficiaries.
Stablecoin Payins is available in the Americas through Circle LLC. Other regions
are not supported for inbound stablecoin payments.
## Travel Rule
Third-party stablecoin payouts that exceed certain thresholds require Travel
Rule data on the sender and the recipient, including originator and beneficiary
identities and, where applicable, Virtual Asset Service Provider (VASP)
information. Circle LLC applies Travel Rule to payouts of \$3,000 USD-equivalent
or more, while Circle Singapore (under MAS PSN02) and Circle SAS (under the EU
Transfer of Funds Regulation) apply it to every third-party payout regardless of
amount. For the complete reference, including identity schemas, payment reason
codes, and VASP handling, see
[Travel rule compliance](/circle-mint/references/travel-rule-compliance).
# How-to: Receive a stablecoin payin
Source: https://developers.circle.com/cpn/stablecoin-payments/howtos/receive-stablecoin-payin
Accept an onchain USDC or EURC payin into your Circle Mint account by creating a payment intent, sharing the deposit address, and confirming the payment.
Use the Stablecoin Payins API to accept an onchain USDC or EURC payment into
your Circle Mint account. You create a payment intent, share the deposit address
that Circle assigns, and then confirm the payment after the customer transfers
funds. For background on how payment intents and payments relate to one another,
see
[Stablecoin payins and payouts](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work).
## Prerequisites
Before you begin:
* Stablecoin Payins is enabled on your Circle Mint account. Activation is scoped
to the Americas (Circle LLC)—contact your Circle representative or reach out
through the [Circle Mint contact form](https://www.circle.com/mint-contact) to
request access.
* You have a Circle Mint sandbox API key. See
[Getting started](/circle-mint/quickstarts/getting-started) for setup.
* You have selected a blockchain that the Stablecoin Payins API supports. The
Payins API supports a narrower set of blockchains than the Payouts API. See
[Supported chains and currencies](/cpn/stablecoin-payments/references/supported-blockchains)
to confirm coverage.
* You have a `merchantWalletId` for the wallet that receives settled funds. You
do not need a funded Mint account—this is a receive flow.
## Steps
Send a `POST` request to `/v1/paymentIntents` to create a payment intent. Use
**continuous** mode, the default, unless you need a fixed-amount,
single-checkout intent.
`purposeOfTransfer` is conditionally required depending on account
configuration. Circle Singapore (`CIRCLE_SG`) and certain Circle LLC
(`CIRCLE_INC`) accounts require it. Use a [payment reason
code](/cpn/references/errors/payment-reason-codes) other than `PMT006`.
Omitting it when required returns error `1177`.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/paymentIntents \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"currency": "USD",
"settlementCurrency": "USD",
"purposeOfTransfer": "PMT001",
"merchantWalletId": "1000999922",
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE"
}
]
}'
```
The response includes the intent `id` and a `timeline` showing the `created`
status. The deposit address is not yet populated.
```json theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE"
}
],
"timeline": [
{
"status": "created",
"time": "2026-04-12T20:13:35.579331Z"
}
],
"type": "continuous",
"purposeOfTransfer": "PMT001",
"createDate": "2026-04-12T20:13:35.578678Z",
"updateDate": "2026-04-12T20:13:35.578678Z"
}
}
```
To create a fixed-amount, single-use intent, set `type` to `transient` and
include an `amount`. The deposit address that Circle assigns is single-use and
the intent expires after `expiresOn`. Send the same `POST /v1/paymentIntents`
request with this body instead:
```json theme={null}
{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"type": "transient",
"amount": {
"amount": "1.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"purposeOfTransfer": "PMT001",
"merchantWalletId": "1000999922",
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE"
}
]
}
```
Circle assigns the deposit address asynchronously after the intent is created.
Use webhooks or polling to retrieve it.
Subscribe to `paymentIntents` notifications. When Circle assigns the address,
you receive a payload whose `paymentMethods[0].address` is populated and whose
`timeline` advances to `pending`.
```json theme={null}
{
"notificationType": "paymentIntents",
"version": 1,
"paymentIntent": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"timeline": [
{ "status": "pending", "time": "2026-04-12T20:13:38.188286Z" },
{ "status": "created", "time": "2026-04-12T20:13:35.579331Z" }
]
}
}
```
Poll `GET /v1/paymentIntents/{id}` until `paymentMethods[0].address` appears in
the response.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"timeline": [
{ "status": "pending", "time": "2026-04-12T20:13:38.188286Z" },
{ "status": "created", "time": "2026-04-12T20:13:35.579331Z" }
],
"type": "continuous"
}
}
```
For memo-based blockchains such as Stellar (XLM) and Hedera (HBAR), the payment
method also includes an `addressTag`. If your customer's wallet enforces memos,
they must include this tag when sending funds.
Display the deposit address (and `addressTag` if applicable) to your customer.
The customer sends USDC or EURC to that address from their wallet. This step
takes place outside the API.
The deposit address is valid only on the blockchain assigned to the payment
intent. Funds sent on any other blockchain are not credited and are
permanently lost.
After the onchain transfer settles, Circle creates a `payment` object linked to
the intent. The intent's own state machine differs by mode:
* **Continuous intents** stay at `active`. The intent timeline does not advance
to `complete`, and the intent's `paymentIds` array is not populated. Reconcile
each settled transfer through the `payments` webhook or by listing payments
with `GET /v1/payments?paymentIntentId={id}`.
* **Transient intents** transition to `complete` after the single expected
transfer settles. The latest timeline entry carries a context of `paid`,
`underpaid`, or `overpaid`, and the intent's `paymentIds` array lists the
settled payments.
A `payments` notification fires whenever an inbound transfer settles. The
payload includes the payment `status`, the originating `fromAddresses`, the
onchain `transactionHash`, and the `paymentIntentId` that links the payment back
to its intent. This works the same for continuous and transient intents.
```json theme={null}
{
"notificationType": "payments",
"version": 1,
"payment": {
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"fromAddresses": {
"chain": "BASE",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
}
```
For transient intents only, a `paymentIntents` notification also fires when the
intent reaches `complete`, with a new `timeline` entry whose `status` is
`complete` and whose `context` describes the outcome (`paid`, `underpaid`, or
`overpaid`). Continuous intents do not produce this notification.
For continuous intents, list the payments scoped to the intent. Each settled
transfer appears as its own payment.
```bash theme={null}
curl -X GET "https://api-sandbox.circle.com/v1/payments?paymentIntentId=e2e90ba3-9d1f-490d-9460-24ac6eb55a1b" \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"fromAddresses": {
"chain": "BASE",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
]
}
```
Polling `GET /v1/paymentIntents/{id}` directly does not work for continuous
intents because the intent's timeline does not advance to `complete` and
`paymentIds` is not populated.
For transient intents, poll `GET /v1/paymentIntents/{id}` until the newest
`timeline` entry has `status` `complete`. The `context` field on that entry
tells you whether the intent was paid, underpaid, or overpaid. Use the IDs in
`paymentIds` to fetch the underlying payment records.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amount": { "amount": "1.00", "currency": "USD" },
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2026-04-12T20:19:24.861094Z"
},
{ "status": "pending", "time": "2026-04-12T20:13:38.188286Z" },
{ "status": "created", "time": "2026-04-12T20:13:35.579331Z" }
],
"type": "transient"
}
}
```
Fetch the payment detail with `GET /v1/payments/{id}` to read the onchain
`transactionHash` and the sender's `fromAddresses`.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/payments/66c56b6a-fc79-338b-8b94-aacc4f0f18de \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"fees": { "amount": "0.01", "currency": "USD" },
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "1.00", "currency": "USD" },
"fromAddresses": {
"chain": "BASE",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
}
```
If a customer abandons checkout, you can expire a transient payment intent
manually by calling `POST /v1/paymentIntents/{id}/expire`. This action applies
only to transient intents.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/paymentIntents/6e4d4047-db14-4c09-b238-1215aee50d03/expire \
-H "Authorization: Bearer $API_KEY"
```
Funds sent to the deposit address after the intent expires are still credited
to your Circle Mint account, but they do not match the original intent. You
may need to contact Circle Support to reconcile these transfers.
## Handle completion contexts (transient intents)
Completion contexts apply only to transient intents, because they have a target
amount to compare against. Continuous intents stay at `active` and never reach
`complete`, so no context is produced.
When a transient intent reaches `complete`, the `context` on the latest timeline
entry tells you how the amount paid compares to what was expected. Each context
calls for a different downstream action.
| Context | Meaning | Typical handling |
| ----------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paid` | The customer paid the expected amount. For transient intents, `amountPaid` equals `amount`. | Fulfill the order. |
| `underpaid` | The customer paid less than the expected `amount` on a transient intent. | Refund the partial payment or contact the customer to top up. See [Refund a stablecoin payin](/cpn/stablecoin-payments/howtos/refund-stablecoin-payin). |
| `overpaid` | The customer paid more than expected. | Refund the excess to the sender, or refund the full amount and ask the customer to retry. |
## See also
* [Stablecoin payins and payouts](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work):
Concepts for payment intents, payments, and refunds.
* [Refund a stablecoin payin](/cpn/stablecoin-payments/howtos/refund-stablecoin-payin):
How to issue a full or partial refund.
* [Supported chains and currencies](/cpn/stablecoin-payments/references/supported-blockchains):
Confirm which blockchains the Payins API supports.
* [Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications):
Subscribe to `paymentIntents` and `payments` webhooks.
# How-to: Refund a stablecoin payin
Source: https://developers.circle.com/cpn/stablecoin-payments/howtos/refund-stablecoin-payin
Issue a full or partial refund against a settled Stablecoin Payins payment intent using the Circle Mint refund APIs.
Use the Stablecoin Payins API to return funds against a payment intent that has
already received at least one settled payment. You can issue a single full
refund, a single partial refund, or multiple partial refunds up to the settled
total. For the mental model that underpins this procedure, see
[Stablecoin payins and payouts](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work).
Refunds are subject to four hard constraints:
* You have 30 days from the payment intent's creation date to start a refund.
* At least one payment on the intent must be settled. You cannot start a refund
while a payment is still pending.
* Once a refund starts, the payment intent transitions to `refunded` and stops
accepting new payins. Treat the intent as terminal and create a new intent for
any future checkout.
* Refunds initiated from the Circle Mint console cannot be canceled after
submission.
Regional availability also affects which payment intents are refundable today.
Refunds for Stablecoin Payins booked through Circle Singapore (CIRCLE\_SG) are
not supported. See [Travel Rule
compliance](/circle-mint/references/travel-rule-compliance) for the data model
that applies to Singapore stablecoin transactions.
## Prerequisites
Before you begin:
* A settled payment exists on the payment intent you want to refund. If you have
not accepted a payin yet, see
[Receive a stablecoin payin](/cpn/stablecoin-payments/howtos/receive-stablecoin-payin).
* You have a refund destination address that you control on the same blockchain
as the original payment. The destination is merchant-supplied; Circle does not
automatically return funds to the source wallet.
* You have a Circle Mint sandbox API key. See
[Getting started](/circle-mint/quickstarts/getting-started) for setup.
## Steps
Before you initiate a refund, retrieve the payment intent and confirm that at
least one payment in `paymentIds` is settled. Send a `GET` request to
`/v1/paymentIntents/{id}`.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b \
-H "Authorization: Bearer $API_KEY"
```
A refund-eligible response includes a populated `paymentIds` array, a non-zero
`amountPaid`, and a `timeline` whose latest entry has `status` `complete` with
`context` `paid`.
```json theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2026-04-12T20:19:24.861094Z"
},
{ "status": "pending", "time": "2026-04-12T20:13:38.188286Z" },
{ "status": "created", "time": "2026-04-12T20:13:35.579331Z" }
],
"type": "continuous",
"createDate": "2026-04-12T20:13:35.578678Z",
"updateDate": "2026-04-12T20:19:24.861094Z"
}
}
```
Send a `POST` request to `/v1/paymentIntents/{id}/refund`. The body specifies
the destination you control, the refund amount, and an idempotency key. The
request uses two amount fields:
* `amount` carries the source-currency basis; only `currency` is required, and
the value is derived from `toAmount`.
* `toAmount` carries the actual refund amount that reaches the destination and
must include both `amount` and `currency`.
For memo-based blockchains, add an `addressTag` to the `destination` object.
Specify an amount lower than the settled total to return a portion of the
payment.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b/refund \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9aed1aab-292a-427f-aae1-e0e358fef1c9",
"destination": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"amount": {
"currency": "USD"
},
"toAmount": {
"amount": "0.50",
"currency": "USD"
}
}
```
Specify the full settled amount to return the entire payment.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b/refund \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9aed1aab-292a-427f-aae1-e0e358fef1c9",
"destination": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"amount": {
"currency": "USD"
},
"toAmount": {
"amount": "1.00",
"currency": "USD"
}
}
```
The response returns a `payment` object with `type` `refund`, `status`
`pending`, and a `depositAddress` that holds the refund destination Circle will
use for the onchain transfer.
```json theme={null}
{
"data": {
"id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6",
"type": "refund",
"status": "pending",
"amount": { "currency": "USD" },
"createDate": "2026-04-13T15:29:58.000000Z",
"updateDate": "2026-04-13T15:29:58.000000Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "0.50", "currency": "USD" },
"depositAddress": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
}
}
}
```
The payment intent's `timeline` immediately gains a `refunded` entry and the
intent becomes terminal. New payins sent to the original deposit address after
this point may not match the intent and can require Circle Support to
reconcile.
Refunds settle asynchronously after the onchain transfer confirms. Use webhooks
or polling to detect settlement.
Subscribe to `payments` notifications. The same channel that delivers inbound
payment events also delivers refund events, discriminated by the `type` field.
```json theme={null}
{
"notificationType": "payments",
"version": 1,
"payment": {
"id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6",
"type": "refund",
"status": "paid",
"amount": { "currency": "USD" },
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "0.50", "currency": "USD" },
"depositAddress": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"transactionHash": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}
}
```
Poll `GET /v1/payments/{id}` with the refund `id` from the previous step.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/payments/3389f4ba-aafd-4eef-aaa2-3292df8f62e6 \
-H "Authorization: Bearer $API_KEY"
```
The refund is settled when `status` is `paid`. The response also includes the
onchain `transactionHash`.
```json theme={null}
{
"data": {
"id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6",
"type": "refund",
"status": "paid",
"amount": { "currency": "USD" },
"fees": { "amount": "0.01", "currency": "USD" },
"createDate": "2026-04-13T15:29:58.000000Z",
"updateDate": "2026-04-13T15:35:12.000000Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "0.50", "currency": "USD" },
"depositAddress": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"transactionHash": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}
}
```
If the original refund was partial and the intent still has refundable balance,
you can issue further partial refunds up to the total settled amount. Each
additional refund still requires the intent to be within 30 days of its creation
date, and each call must use a unique `idempotencyKey`.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b/refund \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "b1d4ef72-3c8f-4cb2-9f0d-3c6f9e2a1d77",
"destination": {
"chain": "BASE",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"amount": {
"currency": "USD"
},
"toAmount": {
"amount": "0.25",
"currency": "USD"
}
}
```
## See also
* [Stablecoin payins and payouts](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work):
Concepts for the refund window and the payment intent state machine.
* [Receive a stablecoin payin](/cpn/stablecoin-payments/howtos/receive-stablecoin-payin):
Accept the payment that you later refund.
* [Travel rule compliance](/circle-mint/references/travel-rule-compliance):
Travel Rule data model for Stablecoin Payouts.
# How-to: Send a stablecoin payout
Source: https://developers.circle.com/cpn/stablecoin-payments/howtos/send-stablecoin-payout
Send USDC or EURC onchain to a third-party recipient with the Stablecoin Payouts API. Covers the Circle LLC (US), Circle Singapore (CIRCLE_SG), and Circle SAS (CIRCLE_FR) flows.
Use the Stablecoin Payouts API to send USDC or EURC onchain to a third-party
recipient, such as vendors, contractors, or customers. For the underlying model,
see
[How stablecoin payments work](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work).
Stablecoin Payouts are for third-party recipients (wallets your business does
not own or control). To move funds between wallets your business owns, see
[Transfer Onchain](/circle-mint/howtos/transfer-on-chain).
## Prerequisites
Before you begin:
* Stablecoin Payouts is enabled on your Circle Mint account. The product is
available through Circle LLC (US), Circle Singapore (CIRCLE\_SG), and Circle
SAS (CIRCLE\_FR). To request activation,
[contact Circle Mint](https://www.circle.com/mint-contact).
* A Circle Mint sandbox API key. See
[Get started with Circle Mint](/circle-mint/quickstarts/getting-started).
* A source wallet ID (`source.id`) with funds available for payouts.
* For CIRCLE\_SG and CIRCLE\_FR: the Virtual Asset Service Provider (VASP) that
holds the recipient's wallet, when the wallet is hosted.
* The destination address is on a blockchain supported by the Stablecoin Payouts
API. See
[Supported chains and currencies](/cpn/stablecoin-payments/references/supported-blockchains).
For XRP destinations (`chain: "XRP"`), the Stablecoin Payouts API supports only
the classic-address format (for example, `rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY`).
The x-address format is not supported. Use the `addressTag` field to specify the
destination tag.
## Steps
The flow differs by Circle entity: Circle LLC (US) applies Travel Rule data at a
\$3,000 USD-equivalent threshold, while Circle Singapore (CIRCLE\_SG) and Circle
SAS (CIRCLE\_FR) require beneficiary identity, wallet ownership, and a payment
reason code on every payout. Circle SAS recipients can include an optional legal
entity identifier (LEI). Select the tab for the Circle entity that books your
payout.
### Step 1. Register the recipient
Add the recipient to your address book with `POST /v1/addressBook/recipients`.
Provide `idempotencyKey`, `chain`, and `address`. The `metadata` fields
(`nickname`, `email`, `bns`) are optional.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/addressBook/recipients \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "BASE",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"nickname": "Acme Corp payout address",
"email": "ap@acme.example",
"bns": "acme.base.eth"
}
}'
```
```json theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "Acme Corp payout address",
"email": "ap@acme.example",
"bns": "acme.base.eth"
},
"status": "pending",
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:16:34.985353Z"
}
}
```
The recipient transitions out of `pending` according to your Mint Console
**Delayed withdrawals** setting. When delayed withdrawals are on, new recipients
stay `inactive` for 24 hours before becoming `active`. When delayed withdrawals
are off, recipients become `active` in seconds.
#### Step 1.1. Wait for active status
The recipient must be `active` before you submit a payout.
Subscribe to `addressBookRecipients` notifications. Circle sends an event when
the recipient is created and when its status changes.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"addressBookRecipient": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
Call `GET /v1/addressBook/recipients/{id}` on an interval until `status` is
`active`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/addressBook/recipients/dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
### Step 2. Send the payout
Call `POST /v1/payouts` with the source wallet, the recipient `id` as the
destination, and the amounts.
Travel Rule applies to payouts of \$3,000 USD-equivalent or more. Include
`source.identities[]` to describe the originator (your organization or your end
customer). See
[Travel rule compliance](/circle-mint/references/travel-rule-compliance) for the
schema and failure modes.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/payouts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345"
},
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
}
}'
```
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/payouts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345",
"identities": [
{
"type": "individual",
"name": "Satoshi Nakamoto",
"addresses": [
{
"line1": "100 Money Street",
"line2": "Suite 1",
"city": "Boston",
"district": "MA",
"postalCode": "01234",
"country": "US"
}
]
}
]
},
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
}
}
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"status": "pending",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:20:30.000Z"
}
}
```
### Step 3. Confirm completion
Wait for the payout to reach `status: complete`.
Subscribe to `payouts` notifications. The payload includes the payout under the
`payout` key.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payouts",
"version": 1,
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
Call `GET /v1/payouts/{id}` on an interval until `status` is `complete`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
The CIRCLE\_SG flow has additional requirements: beneficiary `identity` and
`ownership` on the recipient, `purposeOfTransfer` on the payout, and Travel Rule
applies to payouts of all amounts.
### Step 1. Look up the recipient's VASP
For hosted-wallet recipients, you need the `vaspId` of the Virtual Asset Service
Provider (VASP) that holds the wallet. Call `GET /v1/addressBook/vasps` to list
the VASP records active in your jurisdiction. This endpoint is available only to
CIRCLE\_SG customers.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/addressBook/vasps \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Anchorage Digital"
},
{
"id": "661f9511-f30c-52e5-b660-709766551111",
"name": "Coinhako"
},
{
"id": "772a0622-041d-63f6-c771-810877662222",
"name": "Paxos"
}
]
}
```
If the recipient self-custodies, self-hosted wallets are not yet supported on
the CIRCLE\_SG flow. The recipient must hold the destination wallet at a listed
VASP.
### Step 2. Register the recipient with identity and ownership
Call `POST /v1/addressBook/recipients` with `chain`, `address`, `identity`, and
`ownership`. Use the request body that matches the recipient type.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/addressBook/recipients \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "BASE",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"nickname": "Ada Lovelace"
},
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
```
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/addressBook/recipients \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "BASE",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"nickname": "Acme Corp payout address"
},
"identity": {
"type": "business",
"businessName": "Acme Corp"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
```
```json theme={null}
{
"data": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "Ada Lovelace"
},
"status": "pending",
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:16:34.985353Z"
}
}
```
The recipient transitions from `pending` to `active` after risk review.
You cannot modify `identity` (error `2036`) or `ownership` (error `2037`) on an
existing recipient through PATCH. To change either, register a new recipient.
#### Step 2.1. Wait for active status
Wait for the recipient `status` to become `active`.
Subscribe to `addressBookRecipients` notifications. Circle sends an event when
the recipient is created and when its status changes.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"addressBookRecipient": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
Call `GET /v1/addressBook/recipients/{id}` on an interval until `status` is
`active`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/addressBook/recipients/8755d0ea-14f9-4259-b092-de23c14b6568 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
### Step 3. Send the payout
Call `POST /v1/payouts` with the originator `identities`, the destination, the
amounts, and `purposeOfTransfer`. Travel Rule applies on every CIRCLE\_SG payout
regardless of amount, so `source.identities[]` is always required.
`purposeOfTransfer` is one of `PMT000` to `PMT030` except `PMT006`. See
[payment reason codes](/circle-mint/references/travel-rule-compliance#payment-reason-codes).
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/payouts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345",
"identities": [
{
"type": "business",
"name": "Example Pte. Ltd.",
"addresses": [
{
"line1": "1 Raffles Place",
"line2": "#20-01",
"city": "Singapore",
"district": "Singapore",
"postalCode": "048616",
"country": "SG"
}
]
}
]
},
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
},
"purposeOfTransfer": "PMT001"
}
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"amount": "500.00",
"currency": "USD"
},
"purposeOfTransfer": "PMT001",
"status": "pending",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:20:30.000Z"
}
}
```
### Step 4. Confirm completion
Wait for the payout to reach `status: complete`.
Subscribe to `payouts` notifications. The payload includes the payout under the
`payout` key.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payouts",
"version": 1,
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"amount": "500.00",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"purposeOfTransfer": "PMT001",
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
Call `GET /v1/payouts/{id}` on an interval until `status` is `complete`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "USD"
},
"toAmount": {
"amount": "500.00",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"purposeOfTransfer": "PMT001",
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
Payouts booked through Circle SAS require beneficiary `identity` and `ownership`
on the recipient and `purposeOfTransfer` on the payout. Travel Rule applies to
payouts of all amounts under the EU Transfer of Funds Regulation. Recipients can
include an optional legal entity identifier (`identity.lei`).
### Step 1. Look up the recipient's VASP
For hosted-wallet recipients, you need the `vaspId` of the Virtual Asset Service
Provider (VASP) that holds the wallet. Call `GET /v1/addressBook/vasps` to list
the VASP records active in your jurisdiction. This endpoint is available to
CIRCLE\_FR customers.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/addressBook/vasps \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Anchorage Digital"
},
{
"id": "661f9511-f30c-52e5-b660-709766551111",
"name": "Coinhako"
},
{
"id": "772a0622-041d-63f6-c771-810877662222",
"name": "Paxos"
}
]
}
```
If the recipient self-custodies, self-hosted wallets are not yet supported on
the CIRCLE\_FR flow. The recipient must hold the destination wallet at a listed
VASP.
### Step 2. Register the recipient with identity and ownership
Call `POST /v1/addressBook/recipients` with `chain`, `address`, `identity`, and
`ownership`. Use the request body that matches the recipient type. For business
recipients, include the optional `identity.lei` when you have the beneficiary's
legal entity identifier.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/addressBook/recipients \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "BASE",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"nickname": "Ada Lovelace"
},
"identity": {
"type": "individual",
"firstName": "Ada",
"lastName": "Lovelace"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
```
The legal entity identifier (`identity.lei`) is a 20-character alphanumeric code
(ISO 17442). It is optional for recipients booked through Circle SAS. Circle
validates the format.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/addressBook/recipients \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "BASE",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"nickname": "Example Corp payout address"
},
"identity": {
"type": "business",
"businessName": "Example Corp",
"lei": "529900T8BM49AURSDO55"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
}
}
```
```json theme={null}
{
"data": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "Example Corp payout address"
},
"status": "pending",
"identity": {
"type": "business",
"businessName": "Example Corp",
"lei": "529900T8BM49AURSDO55"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:16:34.985353Z"
}
}
```
The recipient transitions from `pending` to `active` after risk review.
You cannot modify `identity` (error `2036`) or `ownership` (error `2037`) on an
existing recipient through PATCH. To change either, register a new recipient.
#### Step 2.1. Wait for active status
Wait for the recipient `status` to become `active`.
Subscribe to `addressBookRecipients` notifications. Circle sends an event when
the recipient is created and when its status changes.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"addressBookRecipient": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"identity": {
"type": "business",
"businessName": "Example Corp",
"lei": "529900T8BM49AURSDO55"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
Call `GET /v1/addressBook/recipients/{id}` on an interval until `status` is
`active`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/addressBook/recipients/8755d0ea-14f9-4259-b092-de23c14b6568 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "8755d0ea-14f9-4259-b092-de23c14b6568",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"identity": {
"type": "business",
"businessName": "Example Corp",
"lei": "529900T8BM49AURSDO55"
},
"ownership": {
"type": "third_party",
"custody": {
"type": "hosted",
"vaspId": "550e8400-e29b-41d4-a716-446655440000"
}
},
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
### Step 3. Send the payout
Call `POST /v1/payouts` with the originator `identities`, the destination, the
amounts, and `purposeOfTransfer`. Travel Rule applies on every CIRCLE\_FR payout
regardless of amount, so `source.identities[]` is always required.
`purposeOfTransfer` is one of `PMT000` to `PMT030` except `PMT006`. See
[payment reason codes](/circle-mint/references/travel-rule-compliance#payment-reason-codes).
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/payouts \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @body.json
```
```json theme={null}
{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345",
"identities": [
{
"type": "business",
"name": "Example SAS",
"addresses": [
{
"line1": "10 Rue de la Paix",
"city": "Paris",
"postalCode": "75002",
"country": "FR"
}
]
}
]
},
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "EUR"
},
"toAmount": {
"currency": "EUR"
},
"purposeOfTransfer": "PMT001"
}
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "EUR"
},
"toAmount": {
"amount": "500.00",
"currency": "EUR"
},
"purposeOfTransfer": "PMT001",
"status": "pending",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:20:30.000Z"
}
}
```
### Step 4. Confirm completion
Wait for the payout to reach `status: complete`.
Subscribe to `payouts` notifications. The payload includes the payout under the
`payout` key.
```json theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payouts",
"version": 1,
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "EUR"
},
"toAmount": {
"amount": "500.00",
"currency": "EUR"
},
"fees": {
"amount": "0.00",
"currency": "EUR"
},
"networkFees": {
"amount": "0.30",
"currency": "EUR"
},
"purposeOfTransfer": "PMT001",
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
Call `GET /v1/payouts/{id}` on an interval until `status` is `complete`.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29 \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "8755d0ea-14f9-4259-b092-de23c14b6568"
},
"amount": {
"amount": "500.00",
"currency": "EUR"
},
"toAmount": {
"amount": "500.00",
"currency": "EUR"
},
"fees": {
"amount": "0.00",
"currency": "EUR"
},
"networkFees": {
"amount": "0.30",
"currency": "EUR"
},
"purposeOfTransfer": "PMT001",
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
## Failure modes
Failures fall into two categories. Synchronous validation errors return HTTP
4xx, so you fix the request body and retry. Asynchronous risk evaluation accepts
the request with HTTP 201, then transitions the payout to `status: failed`.
Resubmit with a new `idempotencyKey` after correcting the underlying data.
| Code | Type | Cause | Resolution |
| -------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transaction_denied` | Asynchronous | Travel Rule data is missing or invalid. The response carries `riskEvaluation.decision: denied` and `riskEvaluation.reason: 3220`. | Review `source.identities`, the recipient's `identity` and `ownership` (CIRCLE\_SG and CIRCLE\_FR), the `vaspId` (CIRCLE\_SG and CIRCLE\_FR), and `purposeOfTransfer` (CIRCLE\_SG, CIRCLE\_FR, and some Circle LLC (United States) accounts). Resubmit with a new `idempotencyKey`. |
| `insufficient_funds` | Asynchronous | The source wallet does not have enough balance for `amount` plus fees. | Fund the source wallet, then resubmit with a new `idempotencyKey`. |
| `5020` | Synchronous (HTTP 400) | `purposeOfTransfer` is missing or invalid on a CIRCLE\_SG, CIRCLE\_FR, or some Circle LLC (United States) payout. | Provide a valid `PMT` code other than `PMT006`. |
| `2024` to `2037` | Synchronous (HTTP 400) | Address Book validation failures on CIRCLE\_SG and CIRCLE\_FR: missing `identity`, missing `ownership`, missing or disallowed `vaspId`, or PATCH attempted on `identity` or `ownership`. | Add the missing or corrected field on the initial create call. For `2036` and `2037` PATCH errors, register a new recipient instead. |
See [Travel rule compliance](/circle-mint/references/travel-rule-compliance) for
schema details and [API errors](/api-reference/circle-mint/error-codes) for the
full error code reference.
## See also
* [How stablecoin payments work](/cpn/stablecoin-payments/concepts/how-stablecoin-payments-work):
conceptual overview of the payin and payout flows.
* [Travel rule compliance](/circle-mint/references/travel-rule-compliance):
identity, ownership, VASP, and payment-reason-code schemas.
* [Supported chains and currencies](/cpn/stablecoin-payments/references/supported-blockchains):
blockchains and currencies available through Stablecoin Payouts.
* [Transfer Onchain](/circle-mint/howtos/transfer-on-chain): first-party
transfers between wallets your business owns through the Core API.
# Supported blockchains
Source: https://developers.circle.com/cpn/stablecoin-payments/references/supported-blockchains
Blockchains supported by Stablecoin Payins and Stablecoin Payouts in Circle Payments Network.
Stablecoin Payins and Stablecoin Payouts both support USDC and EURC on the
blockchains listed here. For API chain codes and the full USDC and EURC currency
tables, see
[Supported chains and currencies](/circle-mint/references/supported-chains-and-currencies).
Stablecoin Payments supports only USDC and EURC on the blockchains listed here.
Don't send unsupported tokens, such as USDT or bridged USDC, to a deposit
address or recipient address. Doing so might result in a loss of funds.
## Supported blockchains
* Algorand
* Arbitrum
* Arc testnet
* Avalanche C-Chain
* Base
* Codex
* Cronos
* EDGE
* Ethereum
* Hedera
* HyperEVM
* Injective
* Ink
* Linea
* Monad
* Morph
* Noble
* OP Mainnet
* Pharos
* Plume
* Polygon PoS
* Sei
* Solana
* Sonic
* Starknet
* Stellar
* World Chain
* XDC
* XRPL
# Webhook events
Source: https://developers.circle.com/cpn/stablecoin-payments/references/webhook-events
Reference for Stablecoin Payins and Stablecoin Payouts webhook event types, payloads, and the notification subscription system.
Stablecoin Payins and Stablecoin Payouts emit webhook events so your server can
react to deposit-address assignment, settled payments, recipient review
outcomes, and payout status changes. Subscribe to these events instead of
polling for status.
* Subscription endpoint: `POST /v1/notifications/subscriptions`
* Setup:
[Circle APIs notifications quickstart](/circle-mint/references/webhook-notifications)
All payloads use the following envelope structure.
| Envelope field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------- |
| `clientId` | string | Your Circle client identifier. |
| `notificationType` | string | Event type identifier. See [Event types](#event-types). |
| `version` | number | Schema version. Always `1`. |
| `` | object | Event-specific data. The key name varies by event type. |
## Event types
| `notificationType` | Resource key | Trigger |
| ----------------------- | ---------------------- | ---------------------------------------------------------------------------------- |
| `paymentIntents` | `paymentIntent` | Intent created, deposit address assigned, or (transient intents) intent completed. |
| `payments` | `payment` | A payin settles, or a refund changes status. Discriminated by `type`. |
| `addressBookRecipients` | `addressBookRecipient` | Recipient status changes, such as `pending` to `active`. |
| `payouts` | `payout` | Payout status changes, such as `pending`, `complete`, or `failed`. |
## Payment intent events
The `paymentIntents` notification fires when you create an intent, when Circle
assigns the deposit address, and—for transient intents—when the intent reaches
`complete`. Continuous intents stay at `active` and never advance to `complete`;
reconcile their transfers through the `payments` event instead.
Status values observed in `timeline[].status`: `created`, `pending`, `active`,
`complete`.
```json Example paymentIntents payload theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "paymentIntents",
"version": 1,
"paymentIntent": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"type": "continuous",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"timeline": [
{ "status": "pending", "time": "2026-04-12T20:13:38.188286Z" },
{ "status": "created", "time": "2026-04-12T20:13:35.579331Z" }
]
}
}
```
## Payment events
The `payments` notification fires whenever an inbound transfer settles or a
refund changes status. Payments and refunds share this channel and are
discriminated by the `type` field (`payment` or `refund`). The payload links
back to its intent through `paymentIntentId`.
Status values: `pending`, `paid`.
```json Example payments payload theme={null}
{
"clientId": "5b057f1e-743c-4aeb-beeb-ef7b2e16f291",
"notificationType": "payments",
"version": 1,
"payment": {
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"fromAddresses": {
"chain": "BASE",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "BASE",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
}
```
## Address book recipient events
The `addressBookRecipients` notification fires when a recipient's status
changes, such as when review completes and the recipient becomes `active`. Wait
for `active` before you send a payout to the recipient.
Status values: `pending`, `inactive`, `active`, `denied`.
```json Example addressBookRecipients payload theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"addressBookRecipient": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1",
"chain": "BASE",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"status": "active",
"createDate": "2026-05-01T14:16:34.985353Z",
"updateDate": "2026-05-01T14:18:02.123456Z"
}
}
```
## Payout events
The `payouts` notification fires when a payout status changes. The payload
includes the resolved `fees` and onchain `networkFees`.
Status values: `pending`, `complete`, `failed`.
```json Example payouts payload theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payouts",
"version": 1,
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe1"
},
"amount": { "amount": "3000.14", "currency": "USD" },
"toAmount": { "amount": "3000.14", "currency": "USD" },
"fees": { "amount": "0.00", "currency": "USD" },
"networkFees": { "amount": "0.30", "currency": "USD" },
"status": "complete",
"createDate": "2026-05-01T14:20:30.000Z",
"updateDate": "2026-05-01T14:21:12.000Z"
}
}
```
# Crosschain transfers
Source: https://developers.circle.com/crosschain-transfers
Move USDC seamlessly across blockchains with native burn-and-mint protocols and unified balances.
**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.
## What you can build
Move USDC across chains securely and efficiently with tools designed for speed,
capital efficiency, and composability:
* **Protocol-level integration**: Build with [CCTP](/cctp), Circle's
permissionless protocol that burns and mints native USDC across supported
blockchains.
* **Chain-free UX**: A single [Gateway](/gateway) balance that works seamlessly
across chains, enabling instant access to liquidity and a simplified
experience for users.
* **Agentic commerce**: Use Gateway [Nanopayments](/gateway/nanopayments) to
enable gas-free USDC payments down to \$0.000001, purpose-built for AI agents,
usage-based billing, and high-frequency sub-cent transactions.
## Choose your approach
Each product is designed for different crosschain use cases. Pick the approach
that matches your application needs.
Permissionless burn-and-mint protocol for native USDC transfers with fast
and standard options.
Unified USDC balance instantly accessible across supported chains. No manual
bridging required.
## When to use each product
Understanding which tool fits your use case helps you build the right crosschain
experience:
CCTP is best when you need fine-grained control over crosschain transfers or
want to build custom composable flows. It's a permissionless protocol that
enables native USDC transfers through burn-and-mint mechanics. Use CCTP when
you need to integrate crosschain transfers at the smart contract level,
require Fast Transfer for speed-sensitive use cases, or want to trigger
automated actions post-transfer using Hooks.
Gateway is designed for applications where users need instant access to their
USDC across multiple chains without manual bridging. It's perfect for
multichain apps, DEXs with chain abstraction, or any platform where users
should see and spend one unified balance regardless of which chain they're on.
Gateway eliminates the need for liquidity management across chains.
Nanopayments extends Gateway with batched settlement to enable gas-free
USDC payments at sub-cent scale. Use it when your application involves AI
agents paying for resources, per-request billing, machine-to-machine
commerce, or any scenario where per-transaction gas costs would exceed the
payment value.
## Dive deeper
* Explore the [CCTP technical guide](/cctp/technical-guide) to understand Fast
Transfer and Standard Transfer options.
* Read the Gateway quickstarts ([EVM](/gateway/quickstarts/unified-balance-evm),
[Solana](/gateway/quickstarts/unified-balance-solana)) to create and use a
unified USDC balance.
* Explore [Nanopayments](/gateway/nanopayments) to build agentic commerce with
gas-free, sub-cent USDC payments.
* Learn about [CCTP supported blockchains](/cctp/cctp-supported-blockchains) and
[Gateway supported blockchains](/gateway/references/supported-blockchains).
# Digital Asset Accounts
Source: https://developers.circle.com/digital-asset-accounts
Stablecoin-backed account infrastructure for distributors and their business end customers
Digital Asset Accounts lets distributors such as fintechs, platforms, and
marketplaces offer stablecoin-backed accounts to their business end customers.
Circle handles custody, compliance, and regulatory oversight under a
[Direct End-User Custody model](/digital-asset-accounts/concepts/custody-model).
Distributors can focus on building great products without managing digital asset
custody.
Digital Asset Accounts supports USDC and EURC across
[multiple blockchains](/digital-asset-accounts/references/supported-currencies-and-blockchains).
Digital Asset Accounts is a permissioned product. Contact your [Circle
representative](mailto:sales@circle.com) to enable API access and request
sandbox provisioning before you can call the End User Onboarding and Digital
Asset Accounts APIs.
## Key features
Fund accounts through wire transfers and withdraw to bank accounts with
automatic stablecoin minting and redemption
Send and receive stablecoins to and from external blockchain addresses with
full transaction tracking
Move funds instantly between subaccounts in the same program with no
blockchain fees
View real-time balances across all accounts and currencies with
Circle-managed custody
Access a complete audit trail of all deposits, withdrawals, transfers, and
internal movements
Built-in risk ratings, transaction limits, and compliance controls managed
by Circle
## What you can build
Integrate Digital Asset Accounts to power a range of financial services for your
business customers.
Build stablecoin-powered wallet experiences for your business customers. Each
customer gets a dedicated account with fiat on-ramps, crypto transfers, and
real-time balance visibility—all backed by Circle's custody infrastructure.
Enable fast, low-cost payouts to sellers and service providers on your
marketplace. Use internal transfers for instant settlement between buyer and
seller accounts. Offer flexible withdrawal options through wire or crypto.
Give small and medium businesses a stablecoin-based financial operating system.
Your customers can receive payments, hold balances in USDC or EURC, and pay
vendors and partners.
Build cross-border payment solutions that use stablecoins to skip correspondent
banking delays. Your customers can send and receive funds globally with
transparent pricing and near-instant settlement.
## How it works
Use the [End User Onboarding API](/end-user-onboarding) to submit business
information, upload documents, and manage Know Your Business (KYB)
verification programmatically. Once approved, each business entity receives
a dedicated account with subaccounts for holding USDC and EURC.
Customers fund accounts by sending a wire transfer to their dedicated wire
instructions or by depositing stablecoins to a blockchain-specific deposit
address. Wire deposits are automatically minted into the corresponding
stablecoin.
Transfer funds instantly between subaccounts in your program at no cost, or
send stablecoins to external blockchain addresses. Withdraw to bank accounts
through wire transfers with automatic stablecoin redemption.
Query account balances and transaction history through the API. Set up
webhook notifications to receive real-time updates on deposits, withdrawals,
and transfers.
## Get started
Create a business client, complete KYB, and get an active account
Simulate your first deposit in sandbox
Understand accounts, subaccounts, and programs
View supported stablecoins and blockchains
## OpenAPI specifications
The Digital Asset Accounts and End User Onboarding API references are generated
from these OpenAPI specifications:
* `https://developers.circle.com/openapi/accounts.yaml`
* `https://developers.circle.com/openapi/customer-orchestration.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.
# Account structure
Source: https://developers.circle.com/digital-asset-accounts/concepts/account-structure
Understand the account hierarchy, roles, and subaccount model in Digital Asset Accounts
The Digital Asset Accounts hierarchy organizes funds across programs, accounts,
and subaccounts. This structure enables regulatory segregation, per-entity
compliance tracking, and clear fund ownership under the
[Direct End-User Custody model](/digital-asset-accounts/concepts/custody-model).
## Roles
Digital Asset Accounts defines two primary roles:
| Role | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Distributor** | A fintech, platform, or marketplace that integrates with the Digital Asset Accounts API to offer stablecoin-backed account services to their customers. Distributors do not hold custody of funds. |
| **End customer** | A business entity onboarded through the distributor. Each end customer is verified by Circle through KYB (Know Your Business) and receives a dedicated account. |
## Account hierarchy
Digital Asset Accounts organizes funds across three levels:
* **Program**: The top-level container for a distributor's integration. When
Circle provisions your API access, your credentials are scoped to a specific
program, and all accounts and transactions in your integration belong to that
program.
* **Account**: Each onboarded business entity receives an account under your
program. The account is the primary identity for the end customer in Digital
Asset Accounts. Circle performs KYB verification before activating an account,
and each account receives a risk tier that determines its
[transaction limits](/digital-asset-accounts/concepts/limits-and-risk-ratings).
* **Subaccount**: Each account contains one or more subaccounts. Each subaccount
holds a balance in a specific currency (for example, USDC or EURC) and is
created automatically when an account is provisioned.
Subaccounts serve as the unit for:
* **Balance tracking**: Each subaccount maintains its own balance in a single
currency.
* **Transaction history**: Deposits,
[wire withdrawals](/digital-asset-accounts/howtos/wire-deposits-and-withdrawals),
and transfers are recorded against specific subaccounts.
* **Internal transfers**: Funds move between subaccounts in the same program
instantly and at no cost.
## Onboarding flow
End-customer onboarding involves three stages:
1. **Business information submission**: The distributor submits the business
entity's information using the
[End User Onboarding API](/end-user-onboarding). The API provides a JSON
Schema that defines required fields, supports bulk data submission, and
handles document uploads. Required details include the business name,
registration, and beneficial owners.
2. **KYB verification**: Circle reviews the submitted information and performs
compliance checks. If the compliance team needs additional data, the
application moves to `PENDING_CUSTOMER_INFORMATION` and the distributor can
respond through the API. See
[Handle requests for information](/end-user-onboarding/howtos/handle-rfis)
for details.
3. **Account activation**: After KYB is approved, Circle activates the account
and provisions subaccounts for the supported currencies. The account is then
ready to receive deposits and perform transactions.
## API key scoping
Your API key is scoped to your program, which means:
* You can only access accounts and transactions in your own program
* All API operations are automatically restricted to your program's scope
* You cannot access accounts belonging to other distributors
This scoping ensures data isolation between distributors while allowing full
visibility into your own end customers' accounts and transactions.
# Direct end-user custody model
Source: https://developers.circle.com/digital-asset-accounts/concepts/custody-model
Understand how Circle's Direct End-User Custody model works for Stablecoin Accounts
Digital Asset Accounts operates under Circle's Direct End-User Custody model,
where Circle holds custody of digital assets on behalf of end customers while
distributors provide the user interface.
## Direct end-user custody defined
In the Direct End-User Custody model, Circle holds custody of the digital assets
(stablecoins) on behalf of the end customer directly. The distributor acts as a
technology integrator that provides the user interface and experience but does
not take custody of or control the underlying funds.
This model provides several advantages:
* **Regulatory clarity**: Circle, as the custodian, is responsible for
compliance with applicable custody regulations
* **Reduced distributor obligations**: Distributors are not required to obtain
money transmitter licenses or custody licenses for the stablecoin holdings
* **End-customer protection**: Funds are held by a regulated entity with robust
security and compliance infrastructure
## How it works
### Beneficial ownership
Each end customer is the beneficial owner of the stablecoins held in their
account. Although the assets are pooled in Circle's custody infrastructure, the
beneficial ownership is tracked at the individual account level.
### Account segregation
Digital Asset Accounts maintains logical segregation of funds at the
[subaccount level](/digital-asset-accounts/concepts/account-structure). Each
subaccount tracks the balance for a specific end customer and currency. The
underlying custody may use omnibus wallet structures (a single blockchain wallet
that pools assets from multiple customers) for operational efficiency, but
account records ensure that each customer's entitlements are clearly defined and
auditable.
### Distributor responsibilities
Under the Direct End-User Custody model, the distributor is responsible for:
* Providing the user interface for their end customers to interact with their
accounts
* Onboarding end customers through the
[End User Onboarding API](/end-user-onboarding), which handles KYB data
collection and document submission
* Integrating with the Digital Asset Accounts API to enable account operations
such as
[wire deposits and withdrawals](/digital-asset-accounts/howtos/wire-deposits-and-withdrawals)
and [crypto transfers](/digital-asset-accounts/howtos/crypto-transfers)
The distributor is **not** responsible for:
* Holding or controlling customer funds
* Managing compliance or regulatory obligations related to custody
* Performing KYB or KYC verification (Circle handles this)
## Compliance
Circle manages the compliance obligations associated with digital asset custody,
including:
* **KYB verification**: Circle performs Know Your Business verification on all
end customers before activating their accounts
* **Transaction monitoring**: Circle monitors transactions for suspicious
activity and compliance with applicable regulations
* **Risk management**: Circle applies risk ratings and transaction limits based
on the verified profile of each end customer
For more information on risk ratings and limits, see
[Limits and risk ratings](/digital-asset-accounts/concepts/limits-and-risk-ratings).
# Limits and risk ratings
Source: https://developers.circle.com/digital-asset-accounts/concepts/limits-and-risk-ratings
Understand risk tiers, transaction limits, and how risk ratings affect account capabilities
Digital Asset Accounts assigns risk ratings to each end-customer
[account](/digital-asset-accounts/concepts/account-structure) based on the
verified business profile. These ratings determine transaction limits and
capabilities.
## Risk tiers
Circle assigns one of four risk tiers to each end-customer account:
| Tier | Label | Description |
| ---- | ----------- | -------------------------------------------------------------------------------- |
| L | Low | Fully verified entities with established operating history and low-risk profiles |
| M | Medium | Verified entities with standard risk profiles |
| MH | Medium-High | Entities requiring enhanced monitoring due to business type or jurisdiction |
| H | High | Entities with elevated risk factors subject to stricter limits and review |
Risk tiers are assigned during KYB verification as part of Circle's
[compliance obligations](/digital-asset-accounts/concepts/custody-model).
Factors include:
* Business type and industry
* Operating jurisdiction
* Ownership structure
* Transaction volume expectations
## Transaction limits
Each risk tier has associated limits that control the maximum values for
different transaction types. Limits apply at multiple levels:
### Per-transaction limits
The maximum amount allowed for a single transaction. These limits vary by
transaction type:
| Transaction type | Description |
| ----------------- | ----------------------------------------------- |
| Wire deposit | Maximum fiat amount per wire deposit |
| Wire withdrawal | Maximum fiat amount per wire withdrawal |
| Crypto transfer | Maximum stablecoin amount per outbound transfer |
| Internal transfer | Maximum stablecoin amount per internal movement |
Specific limit values are defined in your program agreement and vary by risk
tier. Query the account's limits through the API to retrieve the current values
for each transaction type.
### Rolling limits
Cumulative limits over a rolling time period. The applicable periods (such as
daily, weekly, or monthly) are program-specific and vary by risk tier.
### Balance limits
The maximum stablecoin balance that an account can hold at any point. If a
deposit or transfer would cause the balance to exceed this limit, the
transaction is rejected.
## How ratings change
Risk ratings can change over time based on:
* **Periodic reviews**: Circle periodically reviews account profiles and may
adjust risk ratings based on updated information
* **Transaction patterns**: Unusual transaction patterns may trigger a review
that results in a rating change
* **Updated business information**: Changes to the business profile, such as
expansion to new jurisdictions, may affect the rating
Risk rating changes are managed by Circle's compliance team. Distributors cannot
directly modify the risk rating of an end-customer account.
## Checking limits
The API exposes current limits for each account through
`GET /v1/accounts/{accountId}/limits`. Query this endpoint before initiating
transactions to confirm the operation falls in the account's current limits. If
a transaction exceeds a limit, the API rejects the request with a `422` error.
See the
[Digital Asset Accounts API reference](/api-reference/digital-asset-accounts/all/get-account)
for endpoint details.
# How-to: Create deposit addresses
Source: https://developers.circle.com/digital-asset-accounts/howtos/create-deposit-addresses
Generate blockchain deposit addresses to receive stablecoin deposits
Generate blockchain-specific deposit addresses so your end customers can receive
stablecoins from external wallets. After creating an address, you can list
existing addresses for an account and monitor incoming deposits as they credit
the account balance.
## Prerequisites
Before you begin:
* Obtain an API key for Digital Asset Accounts from Circle.
* Have an onboarded and active account with an `accountId`.
* Install cURL on your development machine.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Steps
### Step 1. Generate a deposit address
Create a deposit address for a specific blockchain. You can create multiple
addresses per account, one for each blockchain.
For valid `chain` values, see
[Supported currencies and blockchains](/digital-asset-accounts/references/supported-currencies-and-blockchains).
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/accounts/addresses/deposit \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "${RANDOM_UUID}",
"currency": "USD",
"chain": "ETH",
"accountId": "${ACCOUNT_ID}"
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "d1e2f3a4-b5c6-7890-def1-234567890abc",
"address": "0xabcdef1234567890abcdef1234567890abcdef12",
"currency": "USD",
"chain": "ETH",
"accountId": "1017381855"
}
}
```
Share the `address` value with your end customer. Any USDC sent to this Ethereum
address is credited to the account.
If you pass an unsupported value in the `chain` field, the API returns a `400`
error:
```json theme={null}
{
"code": 400,
"message": "Unsupported chain value. See supported currencies and blockchains for valid options."
}
```
See the
[supported currencies and blockchains](/digital-asset-accounts/references/supported-currencies-and-blockchains)
reference for valid values.
### Step 2. List deposit addresses
Retrieve all deposit addresses for an account.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/accounts/addresses/deposit?accountId=${ACCOUNT_ID}' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "d1e2f3a4-b5c6-7890-def1-234567890abc",
"address": "0xabcdef1234567890abcdef1234567890abcdef12",
"currency": "USD",
"chain": "ETH",
"accountId": "1017381855"
}
]
}
```
### Step 3. Monitor incoming deposits
When stablecoins arrive at a deposit address, the system detects the transaction
and creates a deposit record. Use the list deposits endpoint to check for
incoming deposits.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/accounts/deposits?accountId=${ACCOUNT_ID}' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "e2f3a4b5-c6d7-8901-ef23-456789abcdef",
"sourceAccountId": "1017381855",
"destination": {
"type": "account",
"id": "1017381855"
},
"amount": {
"amount": "1000.00",
"currency": "USD"
},
"status": "complete",
"createDate": "2026-03-16T08:30:00Z",
"updateDate": "2026-03-16T08:35:00Z"
}
]
}
```
For real-time deposit notifications, configure
[webhook notifications](/api-reference/webhook-endpoints#v1-notifications)
instead of polling the deposits endpoint.
## See also
* [Send crypto transfers](/digital-asset-accounts/howtos/crypto-transfers)
* [Set up webhook notifications](/api-reference/webhook-endpoints#v1-notifications)
# How-to: Send crypto transfers
Source: https://developers.circle.com/digital-asset-accounts/howtos/crypto-transfers
Register recipient addresses and send stablecoins to external blockchain wallets
Register external blockchain addresses and send stablecoins from a Digital Asset
Account to those addresses. After sending, track the outbound transfer as it
moves through processing states and confirms onchain, and manage your recipient
address book by listing or deleting registered addresses.
## Prerequisites
Before you begin:
* Obtain an API key for Digital Asset Accounts from Circle.
* Have an onboarded and active account with a funded balance.
* Have an external blockchain address to receive the transfer.
* Install cURL on your development machine.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Steps
### Step 1. Register a recipient address
Register the external blockchain address where you want to send stablecoins.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/accounts/addresses/recipient \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "${RANDOM_UUID}",
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "ETH",
"description": "Treasury wallet",
"accountId": "${ACCOUNT_ID}"
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "c7d8e9f0-a1b2-3456-7890-abcdef123456",
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "ETH",
"currency": "USD",
"description": "Treasury wallet",
"status": "complete"
}
}
```
### Step 2. Create a transfer
Initiate a transfer to the registered recipient address. Specify the source
account as an `account` type and the destination as a `verified_blockchain`
type. The `verified_blockchain` type indicates a pre-registered external
blockchain address. Include the recipient address ID from the previous step.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/accounts/transfers \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "${RANDOM_UUID}",
"source": {
"type": "account",
"id": "${ACCOUNT_ID}"
},
"destination": {
"type": "verified_blockchain",
"addressId": "${RECIPIENT_ADDRESS_ID}"
},
"amount": {
"amount": "500.00",
"currency": "USD"
}
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "e1f2a3b4-c5d6-7890-ef12-3456789abcde",
"status": "pending",
"amount": {
"amount": "500.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "1017381855"
},
"destination": {
"type": "verified_blockchain",
"id": "c7d8e9f0-a1b2-3456-7890-abcdef123456"
},
"createDate": "2026-03-15T11:00:00Z",
"updateDate": "2026-03-15T11:00:00Z"
}
}
```
If the source account has insufficient funds, the API returns an error. Check
the account balance before creating a transfer.
### Step 3. Check the transfer status
Monitor the transfer until it reaches a terminal state. Transfers move through
`pending`, `complete`, and `failed` states. See
[Transaction states](/digital-asset-accounts/references/transaction-states) for
details.
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/transfers/${TRANSFER_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"id": "e1f2a3b4-c5d6-7890-ef12-3456789abcde",
"status": "complete",
"amount": {
"amount": "500.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "1017381855"
},
"destination": {
"type": "verified_blockchain",
"id": "c7d8e9f0-a1b2-3456-7890-abcdef123456",
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "ETH"
},
"createDate": "2026-03-15T11:00:00Z",
"updateDate": "2026-03-15T11:05:00Z"
}
}
```
### Step 4. Verify the balance
Confirm that the transfer amount has been deducted from the account.
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/${ACCOUNT_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"available": [
{
"amount": "9500.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
## Manage recipient addresses
### List recipient addresses
Retrieve all registered recipient addresses for an account. This endpoint
supports pagination.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/accounts/addresses/recipient?accountId=${ACCOUNT_ID}&pageSize=25' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "c7d8e9f0-a1b2-3456-7890-abcdef123456",
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "ETH",
"currency": "USD",
"description": "Treasury wallet",
"status": "complete"
}
]
}
```
### Delete a recipient address
Remove a recipient address that is no longer needed. Deleted addresses cannot be
used for new transfers.
```shell theme={null}
curl --request DELETE \
--url https://api-sandbox.circle.com/v1/accounts/addresses/recipient/${RECIPIENT_ADDRESS_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
If the deletion is successful, the API returns a `200` response.
Deleting a recipient address does not affect previously completed transfers to
that address. The transaction history is preserved.
## See also
* [Create deposit addresses](/digital-asset-accounts/howtos/create-deposit-addresses)
* [Supported currencies and blockchains](/digital-asset-accounts/references/supported-currencies-and-blockchains)
* [Transaction states](/digital-asset-accounts/references/transaction-states)
* [Set up webhook notifications](/api-reference/webhook-endpoints#v1-notifications)
# How-to: Transfer funds internally
Source: https://developers.circle.com/digital-asset-accounts/howtos/internal-transfers
Move funds instantly between accounts with no blockchain fees
Move funds between two accounts in the same program instantly and with no
blockchain fees. You can verify balances on both the source and destination
accounts before and after the transfer.
## Prerequisites
Before you begin:
* Obtain an API key for Digital Asset Accounts from Circle.
* Have two onboarded and active accounts in the same program, each with an
`accountId`. For details on how accounts relate to programs, see
[Account structure](/digital-asset-accounts/concepts/account-structure).
* Ensure at least one account has a funded balance.
* Install cURL on your development machine.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Steps
### Step 1. Check source and destination balances
Before creating the transfer, verify the balances of both accounts. The source
account must have sufficient funds to cover the transfer amount.
**Check source balance**
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/${SOURCE_ACCOUNT_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"available": [
{
"amount": "10000.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
### Step 2. Create an internal transfer
Initiate the transfer by specifying both the source and destination as `account`
types with their respective account IDs. Internal transfers complete instantly.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/accounts/transfers \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "${RANDOM_UUID}",
"source": {
"type": "account",
"id": "${SOURCE_ACCOUNT_ID}"
},
"destination": {
"type": "account",
"id": "${DEST_ACCOUNT_ID}"
},
"amount": {
"amount": "2500.00",
"currency": "USD"
}
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"status": "complete",
"amount": {
"amount": "2500.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "1017381855"
},
"destination": {
"type": "account",
"id": "1000661542"
},
"createDate": "2026-03-15T12:00:00Z",
"updateDate": "2026-03-15T12:00:00Z"
}
}
```
Internal account-to-account transfers complete instantly and return a `complete`
status in the response. There are no blockchain fees or processing delays.
If the source account has insufficient funds, the API returns an error. Verify
the balance before creating a transfer.
### Step 3. Verify both balances
Confirm that funds have moved correctly by checking the balances of both
accounts.
**Source balance**
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/${SOURCE_ACCOUNT_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"available": [
{
"amount": "7500.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
**Destination balance**
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/${DEST_ACCOUNT_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"available": [
{
"amount": "2500.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
The source account balance decreased by 2,500 USD and the destination account
balance increased by the same amount.
## Look up transfers
### Get a transfer by ID
Retrieve the details of a specific transfer using the transfer ID returned when
you created it.
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/transfers/${TRANSFER_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"status": "complete",
"amount": {
"amount": "2500.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "1017381855"
},
"destination": {
"type": "account",
"id": "1000661542"
},
"createDate": "2026-03-15T12:00:00Z",
"updateDate": "2026-03-15T12:00:00Z"
}
}
```
### List transfers for an account
Retrieve all transfers for an account. This endpoint returns both internal and
external transfers in reverse order and supports pagination.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/accounts/transfers?accountId=${ACCOUNT_ID}&pageSize=25' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"status": "complete",
"amount": {
"amount": "2500.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "1017381855"
},
"destination": {
"type": "account",
"id": "1000661542"
},
"createDate": "2026-03-15T12:00:00Z",
"updateDate": "2026-03-15T12:00:00Z"
}
]
}
```
## See also
* [Account structure](/digital-asset-accounts/concepts/account-structure)
* [Transaction states](/digital-asset-accounts/references/transaction-states)
* [Set up webhook notifications](/api-reference/webhook-endpoints#v1-notifications)
# How-to: Wire deposits and withdrawals
Source: https://developers.circle.com/digital-asset-accounts/howtos/wire-deposits-and-withdrawals
Link bank accounts, deposit fiat, and withdraw stablecoins through wire transfers
Link your end customers' bank accounts to their Digital Asset Accounts so they
can move money between traditional banking and stablecoins. Wire deposits
convert fiat into stablecoins. Wire withdrawals redeem stablecoin balances and
send funds to the linked bank account.
## Prerequisites
Before you begin:
* Obtain an API key for Digital Asset Accounts from Circle.
* Have an onboarded and active account with an `accountId`.
* Install cURL on your development machine.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Steps
### Step 1. Link a bank account
Link your end customer's bank account by providing the bank details and billing
information.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/banks/wires \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"accountId": "${ACCOUNT_ID}",
"idempotencyKey": "${RANDOM_UUID}",
"accountNumber": "123456789",
"routingNumber": "021000021",
"billingDetails": {
"name": "Acme Corp",
"line1": "100 Money Street",
"city": "Boston",
"district": "MA",
"postalCode": "01234",
"country": "US"
},
"bankAddress": {
"bankName": "Chase Bank",
"city": "New York",
"country": "US",
"line1": "270 Park Avenue",
"district": "NY"
}
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "b8a025e7-d5e5-4e5a-8e35-2f3a7d1c9b4a",
"status": "complete",
"description": "CHASE BANK ****6789",
"trackingRef": "CIR3XXXYYY",
"virtualAccountEnabled": false,
"fingerprint": "abc123def456",
"billingDetails": {
"name": "Acme Corp",
"line1": "100 Money Street",
"city": "Boston",
"postalCode": "01234",
"district": "MA",
"country": "US"
},
"bankAddress": {
"bankName": "Chase Bank",
"city": "New York",
"country": "US"
},
"createDate": "2026-03-15T10:00:00Z",
"updateDate": "2026-03-15T10:00:00Z"
}
}
```
### Step 2. Get wire instructions
Retrieve the wire instructions for the linked bank account. Share these with
your end customer so they can send a wire deposit from their bank.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/banks/wires/${WIRE_ID}/instructions?accountId=${ACCOUNT_ID}' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"trackingRef": "CIR3XXXYYY",
"beneficiary": {
"name": "Circle Internet Financial Inc",
"address1": "99 High Street",
"address2": "Boston, MA 02110"
},
"virtualAccountEnabled": false,
"beneficiaryBank": {
"name": "Silicon Valley Bank",
"routingNumber": "121140399",
"accountNumber": "1234567890",
"currency": "USD",
"country": "US"
}
}
}
```
The `trackingRef` is a unique reference code that identifies the wire deposit.
Your customer must include it in their wire transfer so Circle can match the
payment to the correct account.
Your end customer must include the `trackingRef` when sending the wire transfer.
Without this reference, the deposit cannot be matched to the correct account.
### Step 3. Deposit fiat (sandbox simulation)
In the sandbox environment, simulate a wire deposit using the mock payments
endpoint. In production, your end customer sends a real wire transfer using the
instructions from the previous step.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/mocks/payments/wire \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"trackingRef": "${TRACKING_REF}",
"amount": {
"amount": "10000.00",
"currency": "USD"
},
"beneficiaryBank": {
"accountNumber": "1234567890",
"routingNumber": "121140399"
}
}
'
```
**Response**
```json theme={null}
{
"data": {
"trackingRef": "CIR3XXXYYY",
"beneficiaryBank": {
"routingNumber": "121140399",
"accountNumber": "1234567890"
},
"amount": {
"amount": "10000.00",
"currency": "USD"
},
"status": "pending"
}
}
```
### Step 4. Check the balance
Confirm that the deposit has been credited to the account.
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/${ACCOUNT_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json theme={null}
{
"data": {
"available": [
{
"amount": "10000.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
### Step 5. Withdraw to a bank account
To withdraw stablecoins as fiat, create a withdrawal that specifies the amount,
the destination wire account, and the source account.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/accounts/withdrawals \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "${RANDOM_UUID}",
"destination": {
"type": "wire",
"id": "${WIRE_ID}"
},
"amount": {
"amount": "5000.00",
"currency": "USD"
},
"source": {
"type": "account",
"id": "${ACCOUNT_ID}"
}
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sourceAccountId": "1017381855",
"destination": {
"type": "wire",
"id": "b8a025e7-d5e5-4e5a-8e35-2f3a7d1c9b4a"
},
"amount": {
"amount": "5000.00",
"currency": "USD"
},
"toAmount": {
"amount": "5000.00",
"currency": "USD"
},
"status": "pending",
"createDate": "2026-03-15T14:00:00Z",
"updateDate": "2026-03-15T14:00:00Z"
}
}
```
The withdrawal processes asynchronously. Monitor the status by polling the
withdrawals endpoint or by setting up
[webhook notifications](/api-reference/webhook-endpoints#v1-notifications).
If the source account has insufficient funds, the API returns an error:
```json theme={null}
{
"code": 2,
"message": "Insufficient funds. The source account does not have enough balance to complete this withdrawal."
}
```
Verify the account balance before initiating a withdrawal.
## List and retrieve wire accounts
You can query linked wire accounts at any time.
**List all wire accounts for an account**
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/banks/wires?accountId=${ACCOUNT_ID}' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Get a specific wire account**
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/banks/wires/${WIRE_ID} \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
# Fund your digital asset account with a wire deposit
Source: https://developers.circle.com/digital-asset-accounts/quickstarts/fund-account-with-wire-deposit
Simulate a wire deposit and check your account balance using the Digital Asset Accounts API in sandbox
Walk through a basic integration flow: confirm your account, get wire
instructions, simulate a deposit, and verify the balance.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Prerequisites
Before you begin this quickstart, ensure you've:
* Obtained an API key for Digital Asset Accounts from your Circle
representative.
* Obtained an onboarded and active account with an `accountId`. Accounts are
created after a business entity passes KYB verification through the
[End User Onboarding API](/end-user-onboarding). In sandbox, accounts are
pre-provisioned for testing.
* (Recommended) Configured a
[webhook subscription](/api-reference/webhook-endpoints#v1-notifications) for
tracking asynchronous events.
## Step 1: Check your account balance
Confirm the account exists and check the starting balance:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/ACCOUNT_ID \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
Replace `ACCOUNT_ID` with your account ID and `YOUR_API_KEY` with your Bearer
token.
```json theme={null}
{
"data": {
"available": [
{
"amount": "0.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
## Step 2: Get wire instructions
Before you can deposit funds, retrieve the wire instructions for your account.
Wire instructions are the bank routing details your customer uses to send a wire
transfer. You need a linked wire account. See
[Wire deposits and withdrawals](/digital-asset-accounts/howtos/wire-deposits-and-withdrawals)
for the full setup process.
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/banks/wires/WIRE_ID/instructions?accountId=ACCOUNT_ID' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
The response includes a `trackingRef` that must be included in the wire
transfer:
```json theme={null}
{
"data": {
"trackingRef": "CIR3XXXYYY",
"beneficiary": {
"name": "Circle Internet Financial Inc",
"address1": "99 High Street",
"address2": "Boston, MA 02110"
},
"beneficiaryBank": {
"name": "Silicon Valley Bank",
"routingNumber": "121140399",
"accountNumber": "1234567890",
"currency": "USD",
"country": "US"
}
}
}
```
## Step 3: Simulate a wire deposit
In sandbox, simulate a wire deposit using the mock payments endpoint. In
production, your customer sends a real wire transfer using the instructions from
step 2.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/mocks/payments/wire \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"trackingRef": "CIR3XXXYYY",
"amount": {
"amount": "10000.00",
"currency": "USD"
},
"beneficiaryBank": {
"accountNumber": "1234567890",
"routingNumber": "121140399"
}
}'
```
The deposit starts in `pending` status (see
[Transaction states](/digital-asset-accounts/references/transaction-states)) and
settles instantly in sandbox:
```json theme={null}
{
"data": {
"trackingRef": "CIR3XXXYYY",
"amount": {
"amount": "10000.00",
"currency": "USD"
},
"status": "pending"
}
}
```
## Step 4: Verify the balance
After the deposit settles, check that the funds appear in your account:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts/balances/ACCOUNT_ID \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
The balance now reflects the deposited amount:
```json theme={null}
{
"data": {
"available": [
{
"amount": "10000.00",
"currency": "USD"
}
],
"unsettled": []
}
}
```
You now have a funded account in sandbox. See
[Account structure](/digital-asset-accounts/concepts/account-structure) to
understand how programs, accounts, and subaccounts relate.
# Onboard customers
Source: https://developers.circle.com/digital-asset-accounts/quickstarts/onboard-customers
Create a business client, complete KYB verification, and get an active stablecoin account using the End User Onboarding API in sandbox
Walk through the full onboarding flow: create a business client, complete KYB
verification, upload documents, submit the application, and retrieve your active
stablecoin account.
The Digital Asset Accounts API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. Set your API key in the
`Authorization` header using the format `Bearer YOUR_API_KEY`. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox) and
[Going to production](/digital-asset-accounts/references/going-to-production)
for environment details.
## Prerequisites
Before you begin this quickstart, ensure you've:
* Requested and received Digital Asset Accounts sandbox access from your Circle
representative. See
[Sandbox environment](/digital-asset-accounts/references/testing-in-sandbox)
for details.
* Obtained an API key for Digital Asset Accounts from your Circle
representative.
* (Recommended) Configured a
[webhook subscription](/api-reference/webhook-endpoints#v1-notifications) for
tracking asynchronous events.
## Step 1: Create a business client
Create a new business client using the End User Onboarding API. Each client
represents a business entity that goes through KYB verification.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/partner/clients \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"clientName": "My Company ABC1",
"country": "US",
"clientType": "business",
"businessDetails": {
"natureOfBusiness": "paymentProcessorPlatform",
"institutionType": "privateCo"
}
}'
```
Replace `YOUR_API_KEY` with your Bearer token. Use a unique `clientName` for
each new client, because the API rejects duplicate names.
**Keep your API keys safe.** Never share your API key or record it in a publicly
accessible medium such as client-side code or public repositories.
Save the `clientEntityId` and `applicationId` from the response. You need both
values for the remaining steps.
```json theme={null}
{
"data": {
"clientEntityId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"applicationId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
```
## Step 2: Complete business information
Fill out the required business information sections. Replace `{applicationId}`
with the value from step 1 in all of the following requests.
### 2.1. Basic business info
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/basicBusinessInfo \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"businessWebsite": "https://mycompany.example.com",
"businessWebsiteProvided": true,
"businessModelDescription": "Digital payments platform for stablecoin transfers",
"customerTypesDescription": "Small businesses and individual consumers in the US",
"productsAndServicesDescription": "USDC payments, deposits, and withdrawals"
}'
```
### 2.2. Business address
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/physicalBusinessAddress \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"physicalAddressLine1": "1 Main St",
"physicalAddressCity": "New York",
"physicalAddressState": "NY",
"physicalAddressPostalCode": "10001",
"physicalAddressCountry": "US",
"businessAddressRemote": false,
"physicalAddressProofOfAddress": []
}'
```
### 2.3. Business registration
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/businessRegistrationDetails \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"businessName": "My Company ABC1",
"businessNameDifferent": false,
"registrationNumber": "00-0000000",
"taxId": "00-0000000",
"countryOfFormation": "US",
"businessStateOfRegistrationOfFormation": "NY",
"dateOfFormation": "2010-01-01"
}'
```
The `businessName` value must match the `clientName` you used in step 1.
## Step 3: Complete the remaining KYB sections
Six more sections need data before you can submit the application. The following
calls use placeholder data that is valid for sandbox testing.
### 3.1. Entity operations
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/entityOperations \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"entityServesCustomers": true,
"countriesWithMostCustomers": ["US"],
"countriesWithPhysicalPresence": ["US"],
"businessAcceptsPrivacyCoin": false,
"customerBaseByType": { "individuals": "70", "businesses": "30" }
}'
```
### 3.2. Compliance details
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/financialCrimesComplianceDetails \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"complianceOfficerDetailsProvided": false,
"noComplianceOfficerDetailsReason": "Outsourced to external compliance firm",
"lastYearAmlProgramProvided": false,
"noLastYearAmlProgramReason": "Entity less than 1 year old",
"amlTestingReportProvided": false,
"noAmlTestingReportReason": "Entity less than 1 year old"
}'
```
### 3.3. Licensing
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/licensingAndRegistration \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"licenseAndRegistrationRequired": false,
"noLicenseAndRegistrationRequiredReason": "Not required in jurisdiction"
}'
```
### 3.4. Beneficial owners
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/beneficialOwnersDetails \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"beneficialOwnersExist": false,
"beneficialOwnershipExempted": true,
"beneficialOwnershipExemptionReason": "Single owner private company"
}'
```
### 3.5. Intended use of account
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/intendedUseOfAccount \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"accountPurpose": ["paymentProcessingPlatform"],
"accountPurposeDescription": "USDC payments and transfers",
"expectedMonthlyFiatActivity": "from0to100k",
"expectedMonthlyCryptoActivity": "from0to100k",
"expectedProductsForUsage": ["circleMint"],
"sourceOfFunds": ["businessOperatingFunds"],
"sourceOfFundsDescription": "Revenue from platform services",
"financialDocumentsProvided": false,
"noFinancialDocumentReason": "New entity",
"assetsUnderManagement": "from0to10m",
"bankBrokerageCryptoStatement": []
}'
```
### 3.6. Control person
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/individualWithSignificantControlPersons \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '[{
"controllerFirstName": "Jane",
"controllerLastName": "Smith",
"controllerDob": "1985-06-15",
"controllerCitizenship": ["US"],
"controllerIdentificationNumber": "000-00-0000",
"controllerJobTitle": "Director",
"controllerLine1": "1 Main St",
"controllerCity": "New York",
"controllerState": "NY",
"controllerPostalCode": "10001",
"controllerCountry": "US"
}]'
```
## Step 4: Upload documents
Upload six PDF documents required for KYB verification. For sandbox testing,
create a minimal dummy PDF:
```shell theme={null}
printf '%%PDF-1.0\n1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>endobj\nxref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \ntrailer<>\nstartxref\n190\n%%%%EOF' > /tmp/dummy.pdf
```
Upload each document. Save the `documentId` from each response because you need
them to link documents back to their sections.
```shell theme={null}
# Business formation document
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=formation.pdf \
-F datumName=businessFormationDocuments -F issuedCountry=US
# Financial statements
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=financials.pdf \
-F datumName=financialStatements -F issuedCountry=US
# Proof of address
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=address.pdf \
-F datumName=physicalAddressProofOfAddress -F issuedCountry=US
# Bank statement
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=bank.pdf \
-F datumName=bankBrokerageCryptoStatement -F issuedCountry=US
# Ownership chart
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=ownership.pdf \
-F datumName=ownershipChart -F issuedCountry=US
```
### 4.1. Upload the control person's government ID
The control person's government ID requires a `refId` and `documentType`. First,
retrieve the `refId`:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/individualWithSignificantControlPersons \
--header 'Authorization: Bearer YOUR_API_KEY'
```
Find the `refId` value in `data.data[0].refId`, then upload the document:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/documents \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header "X-Idempotency-Key: $(uuidgen)" \
-F fileContent=@/tmp/dummy.pdf -F fileName=govid.pdf \
-F datumName=controllerGovernmentId -F issuedCountry=US \
-F refId={refId} -F documentType=passport
```
### 4.2. Re-save sections with document references
After uploading, link the documents back to their sections by re-saving the
section with the `documentId` values. For example, re-save
`businessRegistrationDetails` with the formation document:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/sections/businessRegistrationDetails \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"businessName": "My Company ABC1",
"businessNameDifferent": false,
"registrationNumber": "00-0000000",
"taxId": "00-0000000",
"countryOfFormation": "US",
"businessStateOfRegistrationOfFormation": "NY",
"dateOfFormation": "2010-01-01",
"businessFormationDocuments": [{ "documentId": "YOUR_DOC_ID" }]
}'
```
Repeat for these sections, adding the relevant `documentId` arrays:
* `physicalBusinessAddress`: add `physicalAddressProofOfAddress`
* `intendedUseOfAccount`: add `bankBrokerageCryptoStatement` and
`financialStatements`
* `beneficialOwnersDetails`: add `ownershipChart`
## Step 5: Submit the application
Verify that all sections show a `complete` status:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId} \
--header 'Authorization: Bearer YOUR_API_KEY'
```
When all sections are complete, submit the application:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/{applicationId}/submit \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header "X-Idempotency-Key: $(uuidgen)" \
--data '{
"endUserIpAddress": "203.0.113.1",
"endUserAgreesToTermsOfService": true
}'
```
In sandbox, the application is auto-approved almost immediately. In production,
KYB verification takes time and may result in rejection.
## Step 6: Retrieve your account
After the application is approved, retrieve the account using the
`clientEntityId` from step 1:
```shell theme={null}
curl --request GET \
--url "https://api-sandbox.circle.com/v1/accounts?clientEntityId={clientEntityId}" \
--header 'Authorization: Bearer YOUR_API_KEY'
```
The response includes your active stablecoin account:
```json theme={null}
{
"data": [
{
"accountId": "1017407114",
"clientEntityId": "992063fe-d52f-3b1e-ab2d-637dd0b0f36e",
"type": "client",
"status": "active",
"description": "Digital Asset Account External Entity Wallet",
"balances": []
}
]
}
```
Your `accountId` is now ready for deposits, withdrawals, and transfers. See
[Wire deposits and withdrawals](/digital-asset-accounts/howtos/wire-deposits-and-withdrawals)
to fund your account.
## Common issues
| Problem | Solution |
| :------------------------------- | :----------------------------------------------------------------------- |
| `500` when creating client | The company name was already used. Use a different `clientName`. |
| `422` on intended use of account | Verify that enum values are exact matches. |
| Section stuck on `incomplete` | Confirm that `taxId` is set in `businessRegistrationDetails`. |
| `400` on submit | A section is not complete, or the `X-Idempotency-Key` header is missing. |
| `422` on government ID upload | Add `documentType=passport` to the upload request. |
# Going to production
Source: https://developers.circle.com/digital-asset-accounts/references/going-to-production
Transition your Digital Asset Accounts integration from sandbox to production
## 1. Request production access
Contact your [Circle representative](mailto:sales@circle.com) to request
production API access. Production access requires a signed agreement with
Circle.
## 2. Replace sandbox URLs with production URLs
Update the base URL in your application from the sandbox endpoint to the
production endpoint.
| Environment | API host |
| :---------- | :------------------------------- |
| Sandbox | `https://api-sandbox.circle.com` |
| Production | `https://api.circle.com` |
## 3. Replace your API key
Replace your sandbox API key with your production API key. Your production API
key is provided by Circle after your production access is approved.
**Keep your production API key safe.** Because the production API key allows you
to work with real funds, treat it with appropriate security protocols. Never
expose it in client-side code or public repositories.
## 4. Account for production settlement times
In sandbox, wire deposits and blockchain transactions settle instantly. In
production, be aware of the following differences:
| Operation | Sandbox behavior | Production behavior |
| ------------------ | ---------------- | --------------------------------------- |
| Wire deposits | Instant | 1-2 business days |
| Wire withdrawals | Instant | 1-2 business days |
| Crypto deposits | Instant | Varies by blockchain (minutes to hours) |
| Crypto transfers | Instant | Varies by blockchain (minutes to hours) |
| Internal transfers | Instant | Instant (same as sandbox) |
Update your application's user interface to set the right expectations for your
end customers about processing times.
## 5. Verify KYB onboarding flow
In sandbox, KYB verification is automatically approved. In production, Circle
performs real KYB checks on your end customers. Ensure your integration handles
the following scenarios:
* **Pending verification**: The account is created but not yet active while
Circle reviews the business entity
* **Rejected verification**: The business entity does not pass KYB checks and
the account is not activated
## 6. Review transaction limits
Sandbox limits differ from production limits. Verify the applicable limits for
your end customers' risk tiers and ensure your integration handles limit errors
gracefully. See
[Limits and risk ratings](/digital-asset-accounts/concepts/limits-and-risk-ratings)
for details.
## 7. Set up webhook notifications
If you haven't already, configure
[webhook notifications](/api-reference/webhook-endpoints#v1-notifications) for
your production environment to receive real-time updates on deposits,
withdrawals, and transfers. Using webhooks reduces the need for polling and
ensures you respond quickly to account events.
## Production checklist
Before going live, confirm that you have completed each of the following items:
* Obtained a production API key from Circle
* Updated all API calls to use the production base URL
* Tested authentication with the production API key
* Handled KYB pending and rejected states in your onboarding flow
* Accounted for real-world settlement times in your user interface
* Configured webhook notifications for production
* Verified that transaction limit errors are handled gracefully
# Supported currencies and blockchains
Source: https://developers.circle.com/digital-asset-accounts/references/supported-currencies-and-blockchains
Currencies and blockchains supported by Digital Asset Accounts
## Supported currencies
| Currency | API currency code | Description |
| -------- | ----------------- | -------------------------------------------------------------------------- |
| USDC | `USD` | A dollar-backed stablecoin issued by Circle, redeemable 1:1 for US dollars |
| EURC | `EUR` | A euro-backed stablecoin issued by Circle, redeemable 1:1 for euros |
## Supported blockchains
Digital Asset Accounts supports the blockchains listed below. Deposit addresses
and outbound transfers are available on each blockchain.
### USDC blockchains
* Algorand
* Aptos
* Arbitrum
* Avalanche C-Chain
* Base
* Celo
* Ethereum
* Hedera
* Noble
* OP Mainnet
* Polygon PoS
* Solana
* Stellar
* Sui
* World Chain
* XRPL
### EURC blockchains
* Avalanche C-Chain
* Base
* Ethereum
* Solana
* Stellar
Supported blockchains may expand over time. Check the API response for the most
up-to-date list of supported blockchain and currency combinations.
## Fiat currencies
Wire deposits and withdrawals support the following fiat currencies:
| Fiat currency | Stablecoin | Direction |
| ------------- | ---------- | ---------------------- |
| USD | USDC | Deposit and withdrawal |
| EUR | EURC | Deposit and withdrawal |
Wire deposits are automatically minted into the corresponding stablecoin. Wire
withdrawals automatically redeem the stablecoin for fiat and initiate a bank
transfer.
# Sandbox environment
Source: https://developers.circle.com/digital-asset-accounts/references/testing-in-sandbox
Use Circle's sandbox environment for safe development and testing of your Digital Asset Accounts integration
The Digital Asset Accounts sandbox environment lets you safely test your
integration without generating real financial transactions or interacting with
live blockchains. The sandbox APIs match those in production, making it easy to
transition when you are ready to go live.
## Request sandbox access
Digital Asset Accounts sandbox access is not automatically provisioned. After
signing up for a Circle sandbox account, contact your Circle representative to
request Digital Asset Accounts access. Circle must enable this access on your
account before you can use the End User Onboarding and Digital Asset Accounts
APIs. Until access is granted, calls to these endpoints return errors.
## API environments and hosts
Use these hosts to access the Digital Asset Accounts API in sandbox and
production environments.
| Environment | API host |
| :---------- | :------------------------------- |
| Sandbox | `https://api-sandbox.circle.com` |
| Production | `https://api.circle.com` |
## Authentication
All API requests require authentication. Include your API key in the
`Authorization` header:
```text theme={null}
Authorization: Bearer YOUR_API_KEY
```
To verify that your API key is correctly set up, you can make a test request to
any read endpoint, such as listing accounts:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/accounts \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
If your API key is valid, you receive a successful response. If the key is
invalid, you receive a `401 Unauthorized` error.
**Keep your API keys safe.** Never share your API key or record it in a publicly
accessible medium such as client-side code or public repositories.
## Sandbox capabilities
The sandbox supports all Digital Asset Accounts API operations:
| Feature | Sandbox behavior |
| ------------------ | ------------------------------------------------------------------------------- |
| Account creation | Accounts are created and KYB is auto-approved |
| Wire deposits | Simulated wire deposits that credit instantly |
| Wire withdrawals | Simulated withdrawals that complete without real fiat movement |
| Crypto deposits | Simulated blockchain deposits |
| Crypto transfers | Simulated outbound transfers that complete without real blockchain transactions |
| Internal transfers | Processed identically to production |
| Webhooks | Notifications are sent to subscriber endpoints |
## Onboard an end customer
To test deposits, withdrawals, or transfers against an end customer account, you
must first onboard a business client through the End User Onboarding API.
See [Onboard customers](/digital-asset-accounts/quickstarts/onboard-customers)
for a step-by-step walkthrough.
## Simulating wire deposits
After onboarding a client and retrieving their account, you can simulate wire
deposits without sending real bank transfers. Create a wire account, retrieve
the wire instructions, then use the mock deposit endpoint to trigger a simulated
deposit:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/mocks/payments/wire \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"trackingRef": "${TRACKING_REF}",
"amount": {
"amount": "10000.00",
"currency": "USD"
},
"beneficiaryBank": {
"accountNumber": "1234567890",
"routingNumber": "121140399"
}
}
'
```
Use the `trackingRef` and `beneficiaryBank` details from the wire instructions
endpoint. The mock deposit is processed immediately and the balance is credited
to the account.
## Simulating crypto deposits
There is no mock endpoint for crypto deposits. Instead, the sandbox
auto-confirms deposits to generated addresses. After creating a deposit address
using the `/v1/accounts/addresses/deposit` endpoint, any test transactions sent
to that address are automatically detected and credited. For details on how
deposit
[transaction states](/digital-asset-accounts/references/transaction-states)
progress, see the transaction states reference.
## Differences between sandbox and production
Be aware of the following differences when testing in sandbox:
* **KYB verification**: Account KYB is automatically approved in sandbox. In
production, KYB verification takes time and may result in rejection.
* **Wire processing**: Wire deposits and withdrawals settle instantly in
sandbox. In production, wire transfers take 1-2 business days.
* **Blockchain confirmations**: Crypto deposits are confirmed instantly in
sandbox. In production, confirmation times depend on the blockchain.
* **Transaction limits**: Sandbox limits differ from production limits. Test
your limit-handling logic, but verify against production limits before launch.
## Moving to production
When you are ready to go live, see
[Going to production](/digital-asset-accounts/references/going-to-production).
# Transaction states
Source: https://developers.circle.com/digital-asset-accounts/references/transaction-states
Learn about the different states for deposits, withdrawals, and transfers
## Deposit states
Deposits represent funds received into a subaccount, either through wire
transfers or blockchain transactions.
| State | Description |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending` | The deposit has been detected and is being processed. For wire deposits, the funds are being received. For crypto deposits, the transaction is awaiting blockchain confirmation. |
| `complete` | The deposit has been fully processed and the stablecoin balance has been credited to the subaccount. |
| `failed` | The deposit could not be processed. This can occur if a wire transfer is rejected or a blockchain transaction fails. |
## Withdrawal states
Withdrawals represent funds sent from a subaccount to an external destination,
either through wire transfers or blockchain transactions.
| State | Description |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending` | The withdrawal has been initiated and is being processed. The stablecoin amount is held from the available balance. |
| `processing` | The withdrawal is actively being executed. For wire withdrawals, the stablecoins are being redeemed and the fiat transfer is being initiated. For crypto withdrawals, the blockchain transaction is being broadcast. |
| `complete` | The withdrawal has been fully processed. For wire withdrawals, the fiat has been sent. For crypto withdrawals, the blockchain transaction has been confirmed. |
| `failed` | The withdrawal could not be processed. The held funds are returned to the available balance. |
## Transfer states
Transfers represent outbound crypto movements to external blockchain addresses.
| State | Description |
| ------------ | ------------------------------------------------------------------------------------- |
| `pending` | The transfer has been initiated and is awaiting processing. |
| `processing` | The blockchain transaction has been broadcast and is awaiting confirmation. |
| `complete` | The blockchain transaction has been confirmed and the transfer is finished. |
| `failed` | The transfer could not be completed. The funds are returned to the available balance. |
**Transfers vs. withdrawals for crypto operations**: A *transfer* sends
stablecoins to an external blockchain address without converting to fiat. A
*withdrawal* redeems stablecoins for fiat currency and sends the fiat through a
wire transfer. Outbound crypto operations use the transfer endpoint, while
outbound fiat operations use the withdrawal endpoint.
## Internal transfer states
Internal transfers move funds between subaccounts in the same program.
| State | Description |
| ---------- | ------------------------------------------------------------------------------------------------------ |
| `complete` | The internal transfer completed instantly. Both the source and destination balances have been updated. |
| `failed` | The internal transfer could not be processed, typically due to insufficient funds. |
Terminal states (`complete`, `failed`) are final states that transactions cannot
transition out of. Set up
[webhook notifications](/api-reference/webhook-endpoints#v1-notifications) to
receive real-time updates when transactions reach these states.
## Notification events
When a transaction transitions to a new state, Digital Asset Accounts sends a
webhook notification. To subscribe, see
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
| Category | Notifications |
| ------------------ | ---------------------------------------------------------- |
| Deposits | Deposit pending, deposit complete, deposit failed |
| Withdrawals | Withdrawal pending, withdrawal complete, withdrawal failed |
| Transfers | Transfer pending, transfer complete, transfer failed |
| Internal transfers | Internal transfer complete |
| Wire events | Wire deposit received, wire withdrawal initiated |
# End User Onboarding
Source: https://developers.circle.com/end-user-onboarding
Programmatically onboard your customers to Circle products using the End User Onboarding API
The End User Onboarding API lets you create and manage onboarding applications
for your customers. Collect business data, upload documents, and submit
applications for compliance review through one REST API. This removes manual
steps as you scale.
Use the End User Onboarding API when you need to bring your own end users onto
Circle's platform. This is required for products where Circle performs Know Your
Business (KYB) verification on your customers, such as
[Digital Asset Accounts](/digital-asset-accounts). The API covers the full
application lifecycle, from draft through approval, including Requests for
Information (RFIs) when the compliance team needs more data.
End User Onboarding API access is enabled per account. Contact your [Circle
representative](mailto:sales@circle.com) to provision sandbox and production
access before calling the API.
## Key features
Retrieve a JSON Schema that defines required fields, validation rules, and
conditional dependencies.
Submit all sections in one call. The API validates first, then saves each in
order.
Upload, list, and delete documents. Files link to the relevant application
fields and entities.
## What you can build
Build onboarding forms in your platform. Use the JSON Schema to render fields
and conditional sections. Your customers stay in your app the whole time.
Migrate existing customers by creating applications and submitting data in bulk.
One request per application fills all sections at once.
Surface RFIs to your operations team or to customers. Webhook notifications
drive real-time updates and automate follow-ups.
## How it works
You create a draft application for your customer. It starts in `DRAFT`
status with a schema that lists the required fields.
You save section data and upload documents. The API validates each
submission against the schema.
When all sections are complete, you submit the application. Circle's
compliance team reviews it and either approves it or issues requests for
information (RFIs).
If the team requests more data, you update the sections or documents and
resubmit. The application moves through statuses until it reaches a final
state.
## Get started
Walk through an application end to end in sandbox
Learn the statuses an application moves through
Walk through the full application lifecycle
Respond to compliance team RFIs and resubmit
# Array sections
Source: https://developers.circle.com/end-user-onboarding/concepts/array-sections
Understand how to work with repeatable entity sections like beneficial owners and directors
Some sections hold repeatable entities such as `beneficial_owners`,
`authorized_signers`, or `directors`. These are **array sections**. The schema
marks them as `type: array` with `items` for each entity. Array sections exist
because a business can have any number of these entities—two beneficial owners,
or five.
Each entity gets its own `refId` so you can update it or attach documents to it
on its own. These sections use different read, save, and document rules than
flat ones.
[Conditional logic](/end-user-onboarding/concepts/conditional-logic) can turn
array sections on or off based on application settings.
## How array sections differ from flat sections
| Operation | Flat section | Array section |
| --------- | -------------------------- | --------------------------------------------------------------------------------- |
| Read | Returns a JSON object | Returns a JSON array; each element includes a `refId` (UUID) |
| Save | `PUT` with a JSON object | `PUT` with a JSON array; include `refId` for existing entities, omit for new ones |
| Delete | Not applicable | `DELETE /sections/{sectionName}/{refId}` removes a single entity |
| Documents | Link with `datumName` only | Link with `datumName` and the entity's `refId` |
## Reading array data
Retrieve array section data with the
[Get section](/api-reference/end-user-onboarding/get-section) endpoint
(`GET /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}`).
The response is a JSON array where each element includes a `refId`:
```json theme={null}
{
"data": [
{
"refId": "550e8400-e29b-41d4-a716-446655440001",
"firstName": "Jane",
"lastName": "Doe",
"ownershipPercentage": "51"
},
{
"refId": "660e8400-e29b-41d4-a716-446655440002",
"firstName": "John",
"lastName": "Smith",
"ownershipPercentage": "49"
}
]
}
```
By default, values like `ownershipPercentage` return as strings. Add
`?typedValues=true` to
[Get section](/api-reference/end-user-onboarding/get-section),
[List sections](/api-reference/end-user-onboarding/list-sections) (with
`includeData=true`), or
[Get all application data](/api-reference/end-user-onboarding/get-all-data) to
get native JSON types.
## Saving array data
When saving, include `refId` for entities you want to update and omit it for new
entities:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/beneficial_owners \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '[
{
"refId": "550e8400-e29b-41d4-a716-446655440001",
"firstName": "Jane",
"lastName": "Doe",
"ownershipPercentage": 60
},
{
"firstName": "Alex",
"lastName": "Johnson",
"ownershipPercentage": 40
}
]'
```
In this example, the first entity is updated (existing `refId`) and the second
is created (no `refId`). Existing entities not included in the array are
preserved, not deleted.
Always include the entity's `refId` when you update it. Without `refId`, the API
adds a new entity instead. That can leave the section `incomplete` if the
duplicate is missing required documents. Delete it with
`DELETE /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}/{refId}`.
## Removing entities
Remove a single entity from an array section with the
[Remove entity](/api-reference/end-user-onboarding/remove-entity) endpoint
(`DELETE /v1/onboarding/partner/applications/{applicationId}/sections/{sectionName}/{refId}`).
The response includes the updated section list with recalculated statuses.
## Document association
When uploading a document for a field in an array entity, include the entity's
`refId` so the document is associated with the correct person or organization.
See [Upload documents](/end-user-onboarding/howtos/upload-documents) for the
full upload workflow.
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=passport_document' \
--form 'refId=550e8400-e29b-41d4-a716-446655440001' \
--form 'issuedCountry=US'
```
Uploading a document for an array entity returns a `documentId`, but that alone
does not fill the entity's document field. Save the array section again with the
`documentId` in the relevant field. Include the same `refId` so the API updates
the existing entity, not a new one.
# Conditional logic
Source: https://developers.circle.com/end-user-onboarding/concepts/conditional-logic
Understand how Nature of Business and JSON Schema conditionals control which fields are required
Conditional logic controls which fields and sections an application needs at a
given point. Different business types need different documents. Data you enter
in one section can also add or remove requirements elsewhere. The schema
expresses this with JSON Schema `if/then` rules, so your integration can adapt
to schema changes instead of hardcoding field requirements.
## Nature of business
The primary conditional driver is **Nature of Business (NOB)**. This
classification is set during application creation and determines which sections
and fields appear. For example, a `cryptoExchange` application may require
different documentation than a `trust` application.
## How conditionals work
The `allOf` array in the schema contains `if/then` blocks:
* The `if` clause checks the value of a parent field (most commonly
`nature_of_business`).
* When the condition is met, the `then` clause adds fields to the `required`
array, making them mandatory.
Supported condition types:
| Condition | Description |
| ------------------ | ---------------------------------------- |
| Exact string match | A field equals a specific value |
| boolean match | A field is `true` or `false` |
| `enum` (OR) | A field matches any value in a list |
| `contains` | An array field contains a specific value |
| Negation | A field does not match a value |
The following example shows an exact string match condition:
```json theme={null}
{
"if": {
"properties": {
"natureOfBusiness": { "const": "cryptoExchange" }
}
},
"then": {
"properties": {
"cryptoExchangeLicenseNumber": {
"type": "string",
"minLength": 1
}
},
"required": ["cryptoExchangeLicenseNumber"]
}
}
```
Here, when `natureOfBusiness` is `cryptoExchange`, the schema requires
`cryptoExchangeLicenseNumber`. The same `if`/`then` pattern works for all types
in the preceding table. Swap in the right JSON Schema keyword in the `if` block:
`enum` for OR lists, `contains` for arrays, `not` for negation.
## Conditional side effects
After
[saving a section](/end-user-onboarding/howtos/create-and-populate-applications#step-4-save-section-data)
that contains a conditional parent field, other fields or sections may become
required. For example, `country=US` may require a state field.
`businessWebsiteProvided=false` may require `noWebsiteReason`.
`beneficialOwnersExist=true` may turn on the beneficial owners section.
[Array sections](/end-user-onboarding/concepts/array-sections) can also activate
this way. The API tells you through three signals:
1. **`sectionsChanged` flag**: Returned in every save response. When `true`, the
section list has changed and you should re-fetch sections.
2. **`fieldsChanged` flag**: Returned in every save response. When `true`, the
server modified one or more field values during save, for example clearing a
field that no longer applies after a conditional change.
3. **Updated `sections` array**: The response includes the current status of all
active sections, not just the one you saved.
Always check `sectionsChanged` after you save section data. If you skip it, you
may submit an incomplete application or miss new required sections. When
`sectionsChanged` or `fieldsChanged` is `true`, re-fetch the schema with
`?resolved=true`. That returns the current required fields for your saved data.
## Mutually exclusive groups
Some conditionals create mutually exclusive branches. For example, different
variants of `basic_business_info` may exist for different NOB values. Only one
branch's fields are required at a time. The schema enforces this automatically.
## Design considerations
* Integrations that skip re-fetching after a `sectionsChanged` response risk
submitting incomplete applications.
* Client-side validation against the schema (`minLength`, `pattern`, `enum`)
before submitting reduces round-trips and improves the user experience.
* Add `?resolved=true` to the schema endpoint. You get only the fields that are
active for your saved data. You do not need to evaluate conditional rules in
your own code.
# How-to: Create and populate applications
Source: https://developers.circle.com/end-user-onboarding/howtos/create-and-populate-applications
Retrieve onboarding applications, discover the schema, and save section data
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Populate an onboarding application by creating a client, reading the schema,
listing sections, and saving field data (individually or in bulk), and cancel
draft applications you no longer need.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[Circle Mint Console](https://console.circle.com).
## Steps
### Step 1. Create a client and initialize the application
#### 1.1. Create a client record and initialize onboarding
Create a client record and initialize an onboarding application in one request:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/partner/clients \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"clientName": "Acme Trust Co.",
"country": "US",
"clientType": "business",
"businessDetails": {
"natureOfBusiness": "trust",
"institutionType": "trust"
}
}'
```
For the full list of request fields, see the
[Create partner client](/api-reference/end-user-onboarding/create-partner-client)
API reference.
**Example response:**
```json theme={null}
{
"data": {
"clientEntityId": "880e8400-e29b-41d4-a716-446655440099",
"applicationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
#### 1.2. Save the application ID
Save the `applicationId`. You'll use it in all subsequent
`/v1/onboarding/partner/applications/...` calls.
Using the same `clientName` and `country` combination returns a `409 Conflict`
error. Use a different `clientName` to create another client in the same
country.
#### 1.3. Retrieve the newly created application
Retrieve the newly created application to inspect its type, sections, and
status:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"type": "ONBOARDING",
"status": "DRAFT",
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "not_started" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
],
"pendingRfis": []
}
}
```
The `type` field identifies the purpose of the application. Partners can create
only `ONBOARDING` applications through the client creation flow shown here.
Other application types may appear in application responses, but they are not
partner-created through this flow. See
[Application types](/end-user-onboarding/references/application-states#application-types)
for the full list of types.
### Step 2. Discover the schema
Retrieve the JSON Schema (draft 2020-12) for your application. It defines every
required field:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/schema \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The schema is a single JSON document. Top-level properties map to section names.
Each section lists its fields, types, and validation rules such as `minLength`,
`maxLength`, `pattern`, `enum`, and `format`.
Use this schema to:
* Enumerate all sections and their fields.
* Drive client-side form rendering and validation.
* Understand which fields are conditional. See
[Conditional logic](/end-user-onboarding/concepts/conditional-logic) for
details.
Pass `?resolved=true` to receive a schema pruned against the application's
current saved data. Conditional branches that have not been activated are
removed, so you only see the fields relevant to the current application state.
### Step 3. List all sections
List all sections with their current completion status:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
Each section has a status: `not_started`, `incomplete`, `complete`, or
`action_required`.
**Example response:**
```json theme={null}
{
"data": {
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "not_started" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
]
}
}
```
To include each section's current field data in the same response, add
`?includeData=true`.
### Step 4. Save section data
Save data for a single section:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/basicBusinessInfo \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
}'
```
The server validates the payload against the JSON Schema before saving. If
validation fails, the API returns a `422` response with per-field errors. Each
error includes a dot-notation path, error code, and message.
By default, array/object/number field values are returned as strings on
[Get section](/api-reference/end-user-onboarding/get-section),
[List sections](/api-reference/end-user-onboarding/list-sections),
[Save section](/api-reference/end-user-onboarding/save-section),
[Bulk save](/api-reference/end-user-onboarding/bulk-save), and
[Get all application data](/api-reference/end-user-onboarding/get-all-data).
Pass `?typedValues=true` on any of these to receive native JSON types instead.
**Example `422` error response:**
```json theme={null}
{
"errors": [
{
"path": "businessWebsite",
"code": "format",
"message": "must be a valid URL"
}
]
}
```
Saving a section may change other sections' statuses. For example, a conditional
section may become required. The response includes a `sectionsChanged` flag.
Check this flag and re-fetch sections as needed.
The response also includes a `fieldsChanged` flag. When `true`, the server
modified one or more field values during save (for example, clearing a field
that is no longer applicable after a conditional change). Re-fetch the section
data if you need to display the current saved state.
Array sections have their own save semantics on top of the preceding flags.
Array sections, such as beneficial owners or controllers, use create-or-update
semantics. Save an existing entity with its `refId`; omitting `refId` creates a
new entity. If you upload a document for an array entity, save the array section
again with the returned `documentId` in the entity's document field. See
[Array sections](/end-user-onboarding/concepts/array-sections) and
[Upload documents](/end-user-onboarding/howtos/upload-documents) for examples.
**Example response:**
```json theme={null}
{
"data": {
"data": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" },
{ "sectionName": "beneficial_owners", "status": "not_started" }
],
"sectionsChanged": false,
"fieldsChanged": false
}
}
```
### Step 5. Bulk save (optional)
If your backend already has all the data, use the bulk save endpoint. It fills
multiple sections in one request:
```shell theme={null}
curl --request PATCH \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/data \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"basicBusinessInfo": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"physicalBusinessAddress": {
"physicalAddressLine1": "123 Main St",
"physicalAddressCity": "San Francisco",
"physicalAddressCountry": "US",
"physicalAddressPostalCode": "94102"
}
}'
```
Validation is atomic. The API checks all sections against the JSON Schema before
saving. If any section fails, no data is written. The response lists errors
across all sections.
After validation passes, sections save one at a time. Saving a section may
activate other conditional sections. If a failure occurs mid-save, earlier
sections may already be persisted.
#### When to use bulk vs. per-section
| Use case | Recommended endpoint |
| -------------------------------------------------- | ----------------------------- |
| Interactive form-based UI (one section at a time) | `PUT /sections/{sectionName}` |
| Programmatic integration with all data available | `PATCH /data` |
| RFI response updating multiple affected sections | `PATCH /data` |
| Reading saved data or deleting individual entities | `GET`/`DELETE /sections/...` |
### Step 6. Read all saved data
To retrieve the complete current state of an application in one request, use the
bulk read endpoint:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/data \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The response `data` field is an object keyed by section name, using the same
structure as the `PATCH /data` request body. This makes it straightforward to
pre-fill a UI or sync application state to your backend. The `sections` array
lists each visible section with its current status. For full parameter and
response details, see the
[Get all application data](/api-reference/end-user-onboarding/get-all-data) API
reference.
**Example response:**
```json theme={null}
{
"data": {
"data": {
"basicBusinessInfo": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"physicalBusinessAddress": {
"physicalAddressLine1": "123 Main St",
"physicalAddressCity": "San Francisco",
"physicalAddressCountry": "US",
"physicalAddressPostalCode": "94102"
}
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "complete" }
]
}
}
```
## Cancel an application
Cancel a `DRAFT` application that is no longer needed:
```shell theme={null}
curl --request DELETE \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
You can only cancel applications in `DRAFT` status. Other statuses return a
`409` error.
## See also
* [Upload documents](/end-user-onboarding/howtos/upload-documents)
* [Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications)
* [Application types and states](/end-user-onboarding/references/application-states#application-types)
* [Conditional logic](/end-user-onboarding/concepts/conditional-logic)
* [Array sections](/end-user-onboarding/concepts/array-sections)
# How-to: Handle requests for information
Source: https://developers.circle.com/end-user-onboarding/howtos/handle-rfis
Respond to compliance team RFIs by updating data, re-uploading documents, and resubmitting applications
Resolve compliance team requests so your application can proceed to approval.
List RFI bundles to see what's needed, update the requested fields or documents,
respond with comments, and resubmit the application.
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[Circle Mint Console](https://console.circle.com).
* Confirmed your application is in `PENDING_CUSTOMER_INFORMATION` status. See
[Application types and states](/end-user-onboarding/references/application-states)
for the full lifecycle.
In the sandbox, Know Your Business (KYB) applications auto-approve on
submission. To get an application into review so an RFI can be issued, submit it
with a reserved business name. See
[Magic numbers for testing](/end-user-onboarding/references/testing-magic-numbers).
## Steps
### Step 1. List RFI bundles
When an application moves to `PENDING_CUSTOMER_INFORMATION`, the `pendingRfis`
array on the application response contains the bundle IDs. List all bundles:
```bash theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"bundles": [
{
"bundleId": "660e8400-e29b-41d4-a716-446655440010",
"status": "PUBLISHED",
"createdAt": "2026-03-26T10:00:00Z",
"rfis": [
{
"rfiId": "770e8400-e29b-41d4-a716-446655440011",
"section": "beneficialOwnerPersons",
"field": "passport_document",
"type": "COLLECT_DOCUMENT",
"status": "PENDING",
"systemComment": "Document is illegible. Please upload a clearer copy.",
"refId": "3de289ef-7947-457d-bbcd-a4fc8dc3b1aa",
"createdAt": "2026-03-26T10:00:00Z"
},
{
"rfiId": "880e8400-e29b-41d4-a716-446655440012",
"section": "businessRegistrationDetails",
"field": "businessWebsite",
"type": "UPDATE_FIELD",
"status": "PENDING",
"systemComment": "Please provide your company website.",
"refId": null,
"createdAt": "2026-03-26T10:01:00Z"
}
]
}
]
}
}
```
Each RFI identifies the `section` and `field` that needs attention. The
`systemComment` explains what the compliance team requires.
Array sections and `refId`: Some sections, such as `beneficialOwnerPersons`,
`beneficialOwnerEntity`, or `trustRelatedPersons`, can contain multiple entries
(for example, multiple beneficial owners). When an RFI targets a field in one of
these sections, the response includes a `refId` identifying which specific
entity the RFI is for. `refId` is `null` for RFIs on single-instance sections
(such as `businessRegistrationDetails`) and for note-type RFIs. Use `refId` when
responding. See [Step 3](#step-3-update-the-requested-data).
### Step 2. View RFI detail and comments
Retrieve the full detail for a specific RFI, including any prior comments:
```bash theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
### Step 3. Update the requested data
For RFIs with type `UPDATE_FIELD` or `NEW_FIELD`, submit the corrected field
data directly to the RFI. The request body shape depends on whether the RFI
targets a single-instance section or an array section.
Single-instance section (`refId` is `null`): submit a flat JSON object with the
field name as the key:
```bash theme={null}
curl --request PATCH \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com"
}'
```
Array section (`refId` is present): submit a single-element JSON array
containing the `refId` from the RFI response alongside the updated field. This
tells the API exactly which entity to update:
```bash theme={null}
curl --request PATCH \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '[{
"refId": "3de289ef-7947-457d-bbcd-a4fc8dc3b1aa",
"trustRelatedPersonLastName": "Smith"
}]'
```
The `refId` in the body must match the `refId` on the RFI. The API rejects
mismatches. Replace the field name with the `field` value from the RFI.
The API validates the submitted value against the application schema and marks
the RFI as responded.
For RFIs with type `COLLECT_DOCUMENT`, re-upload the document instead. When the
RFI has a `refId`, include it in the document upload request to associate the
file with the correct entity. See
[Upload documents](/end-user-onboarding/howtos/upload-documents) for the upload
workflow.
### Step 4. Respond with a comment
Add a comment to an RFI to communicate with the compliance team:
```bash theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/rfis/${RFI_ID}/comments \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"content": "Updated the field as requested."
}'
```
### Step 5. Resubmit the application
After you address all open RFIs, resubmit the application so it moves back to
`SUBMITTED` for another round of review:
```bash theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true
}'
```
This is the same endpoint used during initial submission. For details on
optional fields such as `certificationIds`, see
[Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications#step-2-submit-the-application).
## See also
* [Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications)
* [Application types and states](/end-user-onboarding/references/application-states)
* [Upload documents](/end-user-onboarding/howtos/upload-documents)
* [Magic numbers for testing](/end-user-onboarding/references/testing-magic-numbers)
# How-to: Submit and track applications
Source: https://developers.circle.com/end-user-onboarding/howtos/submit-and-track-applications
Submit applications for review and handle Requests for Information
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Submit a completed application for compliance review and track its progress
through approval. After submission, monitor status changes through webhooks or
polling, and list applications by status as needed.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[Circle Mint Console](https://console.circle.com).
* Completed all application sections (each has `complete` status).
* Uploaded all required documents. See
[Upload documents](/end-user-onboarding/howtos/upload-documents).
## Steps
### Step 1. Discover required certifications (GUI flows only)
Some applications require your end user to actively review and agree to one or
more compliance certifications before you submit on their behalf. Whether you
need to perform this step depends on how your integration is built:
* **API-only integrations:** If you control the submission flow programmatically
and your end users never see the terms directly (for example, you have a
separate agreement process in your own product), you can skip this step and
omit `certificationIds` from the submit request.
* **GUI / end-user flows:** If you are building a UI where end users read and
explicitly agree to Circle's certification terms, you must fetch the active
certifications for the application, present them to the user, and pass the
agreed certification IDs in the submit request.
To retrieve the active certifications for an application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/certifications \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": [
{
"id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"content": "I certify that ..."
}
]
}
```
Display each certification's `content` (HTML) to your end user and collect their
acknowledgment. Then pass every `id` in the `certificationIds` array when you
submit (see step 2 below). If the set of IDs you provide does not exactly match
the application's active certifications, the API returns a `422` error with
details about which IDs are missing or unknown.
### Step 2. Submit the application
Submit an application when all sections have `complete` status:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true,
"certificationIds": [
"9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
]
}'
```
For the full request schema, see the
[Submit application](/api-reference/end-user-onboarding/submit-application) API
reference.
The application moves from `DRAFT` to `SUBMITTED`. If any sections are
incomplete, the server returns a `409` error.
**Example response:**
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"type": "ONBOARDING",
"status": "SUBMITTED"
}
}
```
### Step 3. Track application status
Monitor your application's progress by polling, or use webhook notifications for
real-time updates (see [Webhook endpoints](/api-reference/webhook-endpoints) to
subscribe):
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
See
[Application types and states](/end-user-onboarding/references/application-states)
for a full list of statuses and transitions, and
[Webhook notifications](/end-user-onboarding/references/webhook-notifications)
for the event types and payloads delivered to your subscription.
### Step 4. List all applications
List applications for your partner account. You can filter by status:
```shell theme={null}
curl --request GET \
--url 'https://api-sandbox.circle.com/v1/onboarding/partner/applications?status=PENDING_CUSTOMER_INFORMATION&limit=25' \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The response supports pagination. Use `starting_after` with the `applicationId`
of the last item to fetch the next page.
If the application moves to `PENDING_CUSTOMER_INFORMATION`, the compliance team
has issued one or more Requests for Information (RFIs). See
[Handle requests for information](/end-user-onboarding/howtos/handle-rfis) for
the full RFI workflow.
## See also
* [Application types and states](/end-user-onboarding/references/application-states)
* [Upload documents](/end-user-onboarding/howtos/upload-documents)
* [Handle requests for information](/end-user-onboarding/howtos/handle-rfis)
# How-to: Upload documents
Source: https://developers.circle.com/end-user-onboarding/howtos/upload-documents
Upload, list, and manage supporting documents for onboarding applications
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
Attach supporting files to an onboarding application (such as identity
documents, certificates, or proof of address), then list, download, or delete
them while the application is still in a mutable state.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[Circle Mint Console](https://console.circle.com).
* Confirmed your application is in `DRAFT` or `PENDING_CUSTOMER_INFORMATION`
status.
* Identified which `datumName` fields need files (from the schema).
## Steps
### Step 1. Upload a document
Upload a file and link it to a field in the application:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=authorizedRepGovernmentId' \
--form 'issuedCountry=US' \
--form 'documentType=passport'
```
#### Required fields
| Field | Description |
| ------------- | -------------------------------------------------------------------------------- |
| `fileContent` | Binary file content |
| `fileName` | File name with extension (for example, `passport.pdf`) |
| `datumName` | Schema field name this file satisfies (for example, `authorizedRepGovernmentId`) |
#### Conditionally required fields
| Field | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `refId` | UUID of the entity this file belongs to. Required for [array sections](/end-user-onboarding/concepts/array-sections). |
| `issuedCountry` | ISO 3166-1 alpha-2 country code. Required for identity documents. |
| `documentType` | Form of identity document being uploaded. Required when the schema field includes an `x-documentTypes` extension. Standard government ID fields accept `passport`, `usdl` (United States driver's license), or `usid` (United States state ID). Notarized government ID fields accept `idNotarization`. |
To find which `documentType` values are accepted for a given field, retrieve the
application schema
(`GET /v1/onboarding/partner/applications/{applicationId}/schema`) and inspect
the `x-documentTypes` array on the target field. Fields without
`x-documentTypes` do not require this parameter.
Uploads for array entities need one more step after this request completes.
Uploading a document for an array entity returns a `documentId`, but it does not
complete the entity's document field by itself. Save the array section again
with the returned `documentId` in the relevant field, including the same `refId`
so the API updates the existing entity instead of creating a duplicate. See
[Array sections](/end-user-onboarding/concepts/array-sections) for an example.
**Example response:**
```json theme={null}
{
"data": {
"documentId": "550e8400-e29b-41d4-a716-446655440002"
}
}
```
Uploading to a submitted application returns a `409` error. The application must
be in `DRAFT` or `PENDING_CUSTOMER_INFORMATION`.
### Step 2. List all documents
List all files for an application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
**Example response:**
```json theme={null}
{
"data": {
"documents": [
{
"documentId": "550e8400-e29b-41d4-a716-446655440002",
"fileName": "passport.pdf",
"datumName": "passport_document",
"uploadedAt": "2026-03-25T14:30:00Z"
}
]
}
}
```
### Step 3. Download a document
Download a file you uploaded:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents/${DOCUMENT_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--output document.pdf
```
The response body is binary data with a `Content-Type: application/octet-stream`
header.
### Step 4. Delete a document
Delete a document that is no longer needed:
```shell theme={null}
curl --request DELETE \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents/${DOCUMENT_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
A successful deletion returns `204 No Content`.
You can only delete documents while the application is in `DRAFT` or
`PENDING_CUSTOMER_INFORMATION` status. Deleting a document from a submitted
application returns a `409` error.
## See also
* [Create and populate applications](/end-user-onboarding/howtos/create-and-populate-applications)
* [Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications)
* [Array sections](/end-user-onboarding/concepts/array-sections) (for `refId`
usage)
# Submit onboarding application
Source: https://developers.circle.com/end-user-onboarding/quickstarts/submit-onboarding-application
Submit an onboarding application using the End User Onboarding API in sandbox
Walk through the full application lifecycle: create a client, populate its
sections, upload a document, and submit for review.
The End User Onboarding API base URL is `https://api-sandbox.circle.com` for
sandbox and `https://api.circle.com` for production. All requests require a
Bearer token obtained via Circle key exchange in the `Authorization` header. All
`POST` requests require an `X-Idempotency-Key` header with a client-generated
UUID v4.
## Prerequisites
Before you begin, ensure that you've:
* Obtained an API key for the End User Onboarding API from the
[Circle Mint Console](https://console.circle.com) (**Developer > API Keys**).
Use this key to obtain a Bearer token through the Circle key exchange.
* (Recommended) Set up a webhook subscription for application status changes to
receive real-time updates instead of polling. See
[Webhook endpoints](/api-reference/webhook-endpoints).
## Step 1: Create a client and initialize the application
Create a client and start an onboarding application in one request:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/partner/clients \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"clientName": "Acme Trust Co.",
"country": "US",
"clientType": "business",
"businessDetails": {
"natureOfBusiness": "trust",
"institutionType": "trust"
}
}'
```
For the full request schema, see
[Create Partner Client](/api-reference/end-user-onboarding/create-partner-client).
**Example response:**
```json theme={null}
{
"data": {
"clientEntityId": "880e8400-e29b-41d4-a716-446655440099",
"applicationId": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
Save the `applicationId`. You'll use it in all subsequent steps.
Using the same `clientName` and `country` combination returns a `409 Conflict`
error. Use a different `clientName` to create another client in the same
country.
## Step 2: Discover the schema
The sections and fields in an application aren't fixed. They're defined by a
JSON Schema (draft 2020-12) specific to the business type you set with
`natureOfBusiness` and `institutionType`. Read the schema before you save data
so you know which sections and fields apply. Don't assume field names.
Retrieve the schema for your application:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/schema \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
Top-level properties map to section names, and each section lists its fields,
types, and validation rules. To list sections with their completion status
instead, call
`GET /v1/onboarding/partner/applications/${APPLICATION_ID}/sections`.
For schema discovery, conditional fields, and bulk save, see
[Create and populate applications](/end-user-onboarding/howtos/create-and-populate-applications).
## Step 3: Save section data
Using the field names from the schema, populate a section. This example saves
the `basicBusinessInfo` section for the trust application created in Step 1. The
sections and fields for your application depend on its own schema:
```shell theme={null}
curl --request PUT \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/sections/basicBusinessInfo \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
}'
```
Section and field names vary by business type. Saving a field that isn't in
the section's schema returns a `422` error with per-field details. Use the
schema from Step 2 to see the fields for your application.
The response confirms the section status changed to `complete`:
```json theme={null}
{
"data": {
"data": {
"businessWebsite": "https://example.com",
"businessWebsiteProvided": true
},
"sections": [
{ "sectionName": "basicBusinessInfo", "status": "complete" },
{ "sectionName": "physicalBusinessAddress", "status": "not_started" }
],
"sectionsChanged": false
}
}
```
Repeat for each remaining section. See
[Create and populate applications](/end-user-onboarding/howtos/create-and-populate-applications)
for details on the schema, bulk save, and conditional sections.
## Step 4: Upload a document
Upload a supporting document and link it to a schema field:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/documents \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--form 'fileContent=@passport.pdf' \
--form 'fileName=passport.pdf' \
--form 'datumName=passport_document' \
--form 'issuedCountry=US'
```
The response returns the new document ID:
```json theme={null}
{
"data": {
"documentId": "550e8400-e29b-41d4-a716-446655440002"
}
}
```
See [Upload documents](/end-user-onboarding/howtos/upload-documents) for details
on required fields and linking documents to array entities.
## Step 5: Submit the application
After all sections are `complete` and required documents are uploaded, submit
the application:
```shell theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}" \
--header 'Content-Type: application/json' \
--header 'X-Idempotency-Key: ${IDEMPOTENCY_KEY}' \
--data '{
"endUserIpAddress": "203.0.113.42",
"endUserAgreesToTermsOfService": true
}'
```
If your integration presents compliance certifications directly to end users
(for example, in a GUI where users read and agree to terms), fetch the active
certifications with `GET /v1/onboarding/partner/applications/${APPLICATION_ID}
/certifications` before submitting and include the agreed certification UUIDs
in a `certificationIds` array in the request body. See [Submit and track
applications](/end-user-onboarding/howtos/submit-and-track-applications) for
details.
The application moves to `SUBMITTED`:
```json theme={null}
{
"data": {
"applicationId": "550e8400-e29b-41d4-a716-446655440000",
"status": "SUBMITTED"
}
}
```
## Step 6: Check application status
Poll for the current status or use webhook notifications (if you set up a
subscription in the prerequisites) for real-time updates:
```shell theme={null}
curl --request GET \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID} \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
The application progresses through `IN_REVIEW` and reaches a terminal status of
`APPROVED` or `DENIED`. If the compliance team needs more information, the
status changes to `PENDING_CUSTOMER_INFORMATION`.
For more details, see:
* [Application types and states](/end-user-onboarding/references/application-states):
the full lifecycle of an application
* [Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications):
the submission workflow
* [Handle requests for information](/end-user-onboarding/howtos/handle-rfis):
responding when the compliance team needs more data
# Application types and states
Source: https://developers.circle.com/end-user-onboarding/references/application-states
Reference for all application types and their statuses
Application types and states describe why an application exists, where it is in
the review process, and which actions are available.
## Application types
The application `type` field identifies the purpose of an application.
| Type | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ONBOARDING` | Initial application used to onboard a client. Partners can create this application type. |
| `INFORMATION_UPDATE` | Application used to update customer information and send RFIs. Partners cannot create this application type. |
| `PERIODIC_REVIEW` | Application used to require customers to review and resubmit information. Partners cannot create this application type. |
## Application statuses
| Status | Description |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `DRAFT` | Created but not yet submitted. Data can be added, updated, or deleted. |
| `SUBMITTED` | Submitted for review. No modifications allowed. |
| `IN_REVIEW` | Compliance review in progress (automated and manual checks). |
| `PENDING_CUSTOMER_INFORMATION` | One or more RFIs issued. Partner must update data or documents and resubmit. |
| `APPROVED` | Approved. Terminal state. |
| `DENIED` | Denied. Terminal state. |
| `CANCELLED` | Canceled. Terminal state. |
## Status transitions
See [Application types](#application-types) for definitions of `ONBOARDING`,
`PERIODIC_REVIEW`, and `INFORMATION_UPDATE`.
### `ONBOARDING` applications
```mermaid theme={null}
flowchart LR
DRAFT --> SUBMITTED
DRAFT --> CANCELLED
SUBMITTED --> IN_REVIEW
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
IN_REVIEW --> APPROVED
IN_REVIEW --> DENIED
```
### `PERIODIC_REVIEW` applications
The system creates periodic review applications in `IN_REVIEW`, then moves them
to `DRAFT` when partner action is required.
```mermaid theme={null}
flowchart LR
IN_REVIEW --> DRAFT
DRAFT --> SUBMITTED
DRAFT --> CANCELLED
SUBMITTED --> IN_REVIEW
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
IN_REVIEW --> APPROVED
IN_REVIEW --> DENIED
```
### `INFORMATION_UPDATE` applications
The system creates information update applications in `IN_REVIEW`. They move to
`PENDING_CUSTOMER_INFORMATION` only when partner action is required.
```mermaid theme={null}
flowchart LR
IN_REVIEW --> APPROVED
IN_REVIEW --> CANCELLED
IN_REVIEW --> PENDING_CUSTOMER_INFORMATION
PENDING_CUSTOMER_INFORMATION --> SUBMITTED
SUBMITTED --> IN_REVIEW
```
See
[Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications)
for the submission and RFI workflow.
## Mutable vs. immutable states
Partners can only modify application data (sections, documents) in these states:
* **`DRAFT`**: Initial state after creation. Full read/write access.
* **`PENDING_CUSTOMER_INFORMATION`**: RFIs issued. Partners can update the
sections and documents referenced by the RFIs, then resubmit.
All other states are immutable. API calls that attempt to modify data return a
`409 Conflict` error.
Saving section data in mutable states may trigger conditional changes. See
[Conditional logic](/end-user-onboarding/concepts/conditional-logic) for
details.
## Section statuses
Each section in an application has its own status that tracks data completeness:
| Status | Description |
| ----------------- | ----------------------------------------------------- |
| `not_started` | No data saved for this section |
| `incomplete` | Some data saved but required fields are missing |
| `complete` | All required fields are populated and valid |
| `action_required` | An RFI targets a field in this section; update needed |
Terminal states (`APPROVED`, `DENIED`, `CANCELLED`) are final. Applications
cannot transition out of these states.
# Magic numbers for testing End User Onboarding
Source: https://developers.circle.com/end-user-onboarding/references/testing-magic-numbers
Magic numbers you can use to drive an End User Onboarding application into review, RFI, or denial outcomes in the sandbox
In the sandbox, Know Your Business (KYB) applications auto-approve on submission
so you can build quickly. To test other outcomes—compliance review,
[requests for information (RFIs)](/end-user-onboarding/howtos/handle-rfis), or
denial—use a reserved business name prefix as a magic number.
These values work only in the sandbox (`https://api-sandbox.circle.com`). In
production, they are normal business names. The application then follows the
real review process.
## How it works
Set the legal business name to a value that starts with one of the following
magic-number prefixes. Then
[submit the application](/end-user-onboarding/howtos/submit-and-track-applications).
When the application moves from `DRAFT` to `SUBMITTED`, the sandbox routes it to
that outcome. It does not auto-approve.
* Matching is case-insensitive and only the prefix is reserved. The rest of the
business name is free text (for example
`CIRCLE_SANDBOX_DENY Acme Trading Ltd`).
* Any business name without a recognized prefix follows the default behavior:
the application is approved.
## Magic numbers
| Business name prefix | Resulting outcome | Webhook |
| ----------------------- | -------------------------------------------------------------- | --------------------------------- |
| `CIRCLE_SANDBOX_REVIEW` | application routes to `IN_REVIEW` instead of auto-approving | none on entry |
| `CIRCLE_SANDBOX_DENY` | application is denied (`DENIED`, terminal) | `onboarding_application_denied` |
| *Any other value* | Default sandbox behavior: application is approved (`APPROVED`) | `onboarding_application_approved` |
See
[Application types and states](/end-user-onboarding/references/application-states)
for the full lifecycle and
[Webhook notifications](/end-user-onboarding/references/webhook-notifications)
for the payloads.
`CIRCLE_SANDBOX_REVIEW` moves the application to `IN_REVIEW`. An RFI can only be
issued after that. To test the full RFI flow, ask your Circle contact to issue
an RFI on the application. You then get an
`onboarding_application_rfi_requested` webhook. The application moves to
`PENDING_CUSTOMER_INFORMATION`.
[Handle the RFI](/end-user-onboarding/howtos/handle-rfis) and resubmit.
## Example
Submit an application whose business name requests a denial outcome:
```bash theme={null}
curl --request POST \
--url https://api-sandbox.circle.com/v1/onboarding/partner/applications/${APPLICATION_ID}/submit \
--header "Authorization: Bearer ${YOUR_API_KEY}"
```
With the business name set to `CIRCLE_SANDBOX_DENY Acme Trading Ltd`, the
application transitions to `DENIED` and your registered endpoint receives an
`onboarding_application_denied`
[webhook](/end-user-onboarding/references/webhook-notifications).
# Webhook notifications
Source: https://developers.circle.com/end-user-onboarding/references/webhook-notifications
Reference for End User Onboarding webhook event types with example payloads.
End User Onboarding sends webhooks when an application's status changes. Use
them for real-time updates instead of polling
[`GET /v1/onboarding/partner/applications/{id}`](/api-reference/end-user-onboarding/get-application).
Notifications arrive as Amazon SNS messages on the
[v1 notification system](/api-reference/webhooks#notification-api-versions). To
register a webhook, create an End User Onboarding subscription in the
[Circle Console](https://console.circle.com) under **Developer >
Subscriptions**. Then follow
[Set up a webhook endpoint](/api-reference/webhook-endpoints#v1-notifications).
## When notifications fire
Webhooks fire only for outcomes you cannot see in your own API response. For
example, `DRAFT` → `SUBMITTED` from a submit call does not send a webhook. The
HTTP response already includes the result.
| Notification type | Fires when the application transitions to | From |
| -------------------------------------- | ----------------------------------------- | ---------------------- |
| `onboarding_application_rfi_requested` | `PENDING_CUSTOMER_INFORMATION` | `IN_REVIEW` |
| `onboarding_application_approved` | `APPROVED` | `IN_REVIEW` |
| `onboarding_application_denied` | `DENIED` | `IN_REVIEW` |
| `onboarding_application_cancelled` | `CANCELLED` | `DRAFT` or `IN_REVIEW` |
| `onboarding_periodic_review_requested` | `DRAFT` | `IN_REVIEW` |
`onboarding_application_rfi_requested` signals that the compliance team has
issued one or more Requests for Information (RFIs). See
[Handle requests for information](/end-user-onboarding/howtos/handle-rfis) for
the RFI workflow, and
[Application types and states](/end-user-onboarding/references/application-states)
for the full lifecycle.
## Payload
Every End User Onboarding notification carries the same flat payload.
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------ |
| `clientId` | UUID | Identifier of the partner the application belongs to. Used to route the notification to your endpoint. |
| `notificationType` | string | The event type, for example `onboarding_application_rfi_requested`. |
| `version` | number | Notification system version. Always `1`. |
| `applicationId` | UUID | Identifier of the affected onboarding application. |
| `status` | string | The application's new status. |
| `previousStatus` | string | The application's status immediately before this transition. |
`status` and `previousStatus` use the same values as
[Application types and states](/end-user-onboarding/references/application-states).
Internal review sub-states are collapsed to `IN_REVIEW`, so a transition out of
review reports `previousStatus` as `IN_REVIEW`.
## Event types
### `onboarding_application_rfi_requested`
The compliance team issued one or more RFIs. The application moved to
`PENDING_CUSTOMER_INFORMATION`. Fetch open RFIs with
[List RFIs](/api-reference/end-user-onboarding/list-rfis). The application's
`pendingRfis` array also lists the bundle IDs. Respond to each RFI, then
resubmit.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_rfi_requested",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "PENDING_CUSTOMER_INFORMATION",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_approved`
Compliance review completed and the application was approved. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_approved",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "APPROVED",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_denied`
Compliance review completed and the application was denied. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_denied",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "DENIED",
"previousStatus": "IN_REVIEW"
}
```
### `onboarding_application_cancelled`
The application was canceled. `previousStatus` reflects the application's status
before cancellation and depends on the application type. This is a terminal
state.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_application_cancelled",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "CANCELLED",
"previousStatus": "DRAFT"
}
```
### `onboarding_periodic_review_requested`
A periodic review application moved from `IN_REVIEW` back to `DRAFT`, making it
editable. Use this notification to start the partner response flow for the
review request.
```json theme={null}
{
"clientId": "880e8400-e29b-41d4-a716-446655440099",
"notificationType": "onboarding_periodic_review_requested",
"version": 1,
"applicationId": "6bb28d72-8a8e-4afc-9086-66f89af954eb",
"status": "DRAFT",
"previousStatus": "IN_REVIEW"
}
```
## See also
* [Application types and states](/end-user-onboarding/references/application-states)
* [Submit and track applications](/end-user-onboarding/howtos/submit-and-track-applications)
* [Handle requests for information](/end-user-onboarding/howtos/handle-rfis)
* [Magic numbers for testing](/end-user-onboarding/references/testing-magic-numbers)
# Circle Gateway
Source: https://developers.circle.com/gateway
Circle Gateway enables a unified USDC balance across multiple blockchains.
Deposit USDC to non-custodial Gateway Wallet contracts on any supported source
blockchain, then mint USDC instantly (\<500 ms) on any destination blockchain
using a single API call.
Gateway is fully permissionless, and you can start integrating with it
immediately with no sign-up needed. Check out the quickstart guides for
[EVM](/gateway/quickstarts/unified-balance-evm) and
[Solana](/gateway/quickstarts/unified-balance-solana).
**Use
[Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit)
to simplify Gateway integrations.**
Unified Balance Kit handles deposit, transfer, and spend flows so you can build
Gateway-powered features in just a few lines of code.
## Key features
Hold USDC across multiple blockchains and access it as a single balance on
any supported destination blockchain
Transfer USDC in under 500 ms after your balance is established, with no
waiting for source blockchain finality
Retain full ownership of deposited USDC with signature-based authorization
and a 7-day trustless withdrawal option
Authorize transfers from smart contracts and smart contract wallets with
ERC-1271, without requiring a separate EOA delegate. See [ERC-1271
programmable authorization](/gateway/references/erc-1271).
## What you can build
Gateway enables applications that require instant access to USDC across
blockchains. Here are some common use cases:
Build applications where users interact with USDC without worrying about which
blockchain it's on. Users deposit once and access their balance instantly on any
supported blockchain.
Provide instant liquidity across blockchains without maintaining separate
balances on each chain. Consolidate USDC holdings and access them where needed.
Route payments to any supported blockchain instantly. Accept USDC on one
blockchain and settle on another without delays.
Reduce working capital requirements by consolidating USDC across blockchains
into a unified balance that's accessible anywhere.
Power AI agents and machine-to-machine systems with gasless, sub-cent USDC
payments. See [Agent nanopayments](/agent-stack/agent-nanopayments) for the
agent-builder workflow.
## Get started
Build a script to deposit USDC on multiple blockchains and transfer it
instantly to a destination blockchain
Authorize Gateway transfers with ERC-1271 using a Circle SCA
Receive real-time notifications for Gateway events on your registered wallet
addresses
View the blockchains where you can deposit and mint USDC with Gateway
## 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) |
# How-to: Create a unified USDC balance
Source: https://developers.circle.com/gateway/howtos/create-unified-usdc-balance
Circle Gateway allows you to establish a unified USDC balance consisting of USDC
stored on multiple source chains. Once established, you can transfer this
balance instantly to any destination chain.
This guide demonstrates how to establish a unified USDC balance by depositing
USDC into the [Gateway Wallet contract](/gateway/references/contract-addresses).
You can perform this action on multiple chains to establish a chain-abstracted
balance.
Select a tab below for EVM or Solana-specific instructions.
## 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 testnet wallet with USDC and native tokens
* Created a TypeScript project and have `viem` installed
* You've set up a `.env` file with the following variables:
```text .env theme={null}
EVM_PRIVATE_KEY={YOUR_PRIVATE_KEY}
```
## Steps
Follow these steps to establish a unified USDC balance on Arc Testnet. You can
adapt this example for any
[supported EVM chain](/gateway/references/supported-blockchains#testnet).
### Step 1. Approve the Gateway Wallet to transfer USDC from your address
Create a new file called `deposit.ts` in the root of your project, and add the
following code to it. This code calls the `approve()` method on the USDC
contract to allow the Gateway Wallet contract to transfer USDC from your wallet.
```ts deposit.ts theme={null}
import {
createPublicClient,
createWalletClient,
getContract,
http,
erc20Abi,
formatUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
/* Constants */
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const USDC_ADDRESS = "0x3600000000000000000000000000000000000000";
const DEPOSIT_AMOUNT = 10_000_000n; // 10 USDC (6 decimals)
const gatewayWalletAbi = [
{
type: "function",
name: "deposit",
inputs: [
{ name: "token", type: "address" },
{ name: "value", type: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
// Set up clients
const publicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: arcTestnet,
transport: http(),
});
// Get contract instances
const usdc = getContract({
address: USDC_ADDRESS,
abi: erc20Abi,
client: walletClient,
});
const gatewayWallet = getContract({
address: GATEWAY_WALLET_ADDRESS,
abi: gatewayWalletAbi,
client: walletClient,
});
// Approve Gateway Wallet to spend USDC
console.log(`Approving ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC...`);
const approvalTx = await usdc.write.approve(
[gatewayWallet.address, DEPOSIT_AMOUNT],
{ account },
);
await publicClient.waitForTransactionReceipt({ hash: approvalTx });
console.log(`Approved: ${approvalTx}`);
```
### Step 2. Call the deposit method on the Gateway Wallet contract
Add the following code to the `deposit.ts` file to call the
[`deposit()`](/gateway/references/contract-interfaces-and-events#deposit) method
on the Gateway Wallet contract. Note that you must use the `deposit()` method
and not the standard transfer on the USDC contract.
**Warning:** Directly transferring USDC to the Gateway Wallet contract with a
standard ERC-20 transfer will result in loss of that USDC. You must use one of
the deposit methods on the wallet contract to get a unified USDC balance.
```ts deposit.ts theme={null}
console.log(
`Depositing ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC to Gateway Wallet...`,
);
const depositTx = await gatewayWallet.write.deposit(
[usdc.address, DEPOSIT_AMOUNT],
{ account },
);
await publicClient.waitForTransactionReceipt({ hash: depositTx });
console.log(`Deposit tx: ${depositTx}`);
```
### Step 3. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env deposit.ts
```
### Step 4. Wait for the required number of block confirmations
Wait for the
[required number of block confirmations](/gateway/references/supported-blockchains#required-block-confirmations).
Once the deposit transaction is final, your unified balance reflects the amount
from Arc Testnet. If you have balances on other chains, the total balance is the
sum of all the USDC from deposit transactions across all supported chains that
have reached finality.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Prepared a Solana Devnet wallet with the keypair exported as a JSON array
* Funded your testnet wallet with USDC and native tokens
* Created a TypeScript project and have the following dependencies installed:
* `@solana/web3.js`
* `@solana/spl-token`
* `@coral-xyz/anchor`
* `bn.js`
* You've set up a `.env` file with the following variable:
```text .env theme={null}
SOLANA_PRIVATE_KEYPAIR={YOUR_SOLANA_KEYPAIR_ARRAY}
```
To generate a new Solana keypair, run:
```shell theme={null}
solana-keygen new -o keypair.json --no-bip39-passphrase
```
Copy the byte array from the JSON file to your `.env` file.
If your wallet exports a private key hash instead, you can use `bs58` to convert
it:
```ts TypeScript theme={null}
const bytes = bs58.decode({ YOUR_PRIVATE_KEY_HASH });
console.log(JSON.stringify(Array.from(bytes)));
```
## Steps
Follow these steps to establish a unified USDC balance on Solana. The high level
steps are the same for EVM chains, consult the EVM example for adding additional
USDC to your unified balance from EVM chains.
### Step 1. Set up your Solana client
Create a new file called `deposit.ts` in the root of your project and add the
following code. This code sets up the Solana connection and wallet client.
```ts deposit.ts theme={null}
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import BN from "bn.js";
/* Constants */
const GATEWAY_WALLET_ADDRESS = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const RPC_ENDPOINT = "https://api.devnet.solana.com";
const DEPOSIT_AMOUNT = new BN(10_000_000); // 10 USDC (6 decimals)
/* Gateway Wallet IDL (partial, for deposit only) */
const gatewayWalletIdl = {
address: GATEWAY_WALLET_ADDRESS,
metadata: {
name: "gatewayWallet",
version: "0.1.0",
spec: "0.1.0",
},
instructions: [
{
name: "deposit",
discriminator: [22, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "owner", signer: true },
{ name: "gatewayWallet" },
{ name: "ownerTokenAccount", writable: true },
{ name: "custodyTokenAccount", writable: true },
{ name: "deposit", writable: true },
{ name: "depositorDenylist" },
{ name: "tokenProgram" },
{ name: "systemProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [{ name: "amount", type: "u64" }],
},
],
};
// Get keypair from environment
if (!process.env.SOLANA_PRIVATE_KEYPAIR)
throw new Error("SOLANA_PRIVATE_KEYPAIR not set");
const keypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.SOLANA_PRIVATE_KEYPAIR)),
);
// Set up connection and public keys
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
const owner = keypair.publicKey;
```
### Step 2. Set up Anchor provider and program and derive PDAs
Add the following code to `deposit.ts` to initialize the Anchor client and
derive the Program Derived Addresses (PDAs) required by the Gateway Wallet.
```ts deposit.ts theme={null}
// Set up Anchor provider and program
const anchorWallet = new Wallet(keypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const program = new Program(gatewayWalletIdl, provider);
// Derive PDAs
const pdas = {
wallet: PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_wallet"))],
programId,
)[0],
custody: PublicKey.findProgramAddressSync(
[
Buffer.from(utils.bytes.utf8.encode("gateway_wallet_custody")),
usdcMint.toBuffer(),
],
programId,
)[0],
deposit: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
programId,
)[0],
denylist: PublicKey.findProgramAddressSync(
[Buffer.from("denylist"), owner.toBuffer()],
programId,
)[0],
};
```
### Step 3. Call the deposit instruction on the Gateway Wallet program
Add the following code to `deposit.ts` to call the deposit instruction on the
Gateway Wallet program. Note that you must use the Gateway Wallet's deposit
instruction and not a standard SPL token transfer.
Directly transferring USDC to the Gateway Wallet program with a standard SPL
token transfer will result in loss of that USDC. You must use the deposit
instruction on the wallet program to get a unified USDC balance.
```ts deposit.ts theme={null}
// Deposit USDC into Gateway Wallet
console.log(
`Depositing ${Number(DEPOSIT_AMOUNT.toString()) / 1_000_000} USDC to Gateway Wallet...`,
);
const tx = await program.methods
.deposit(DEPOSIT_AMOUNT)
.accountsPartial({
payer: owner,
owner: owner,
gatewayWallet: pdas.wallet,
ownerTokenAccount: getAssociatedTokenAddressSync(usdcMint, owner),
custodyTokenAccount: pdas.custody,
deposit: pdas.deposit,
depositorDenylist: pdas.denylist,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([keypair])
.rpc();
// Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction(
{
signature: tx,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed",
);
console.log(`Deposit tx: ${tx}`);
```
### Step 4. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env deposit.ts
```
### Step 5. Wait for finality
Wait for the deposit transaction to reach finality. On Solana Devnet, finality
is typically reached in a few seconds. Once the deposit transaction is final,
your unified balance reflects the amount from Solana. If you have balances on
other chains, the total balance is the sum of all the USDC from deposit
transactions across all supported chains that have reached finality.
# How-to: Transfer with the Forwarding Service
Source: https://developers.circle.com/gateway/howtos/forwarding-service
Transfer USDC crosschain using the Forwarding Service to automatically handle destination chain minting
Transfer your unified USDC balance to a destination chain without needing a
wallet or gas on that chain. The
[Forwarding Service](/gateway/references/forwarding-service) handles the
destination chain mint automatically.
This guide demonstrates how to estimate fees, create a burn intent, submit it
with forwarding enabled, and poll for transfer completion.
Select a tab below for EVM or Solana destination instructions.
## 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 the
[supported Testnets](/gateway/references/supported-blockchains#testnet) of
your choice to your wallet (this guide uses Arc Testnet and Base Sepolia)
* Funded your testnet wallet with native tokens on the source chain (this guide
uses Arc Testnet). With the Forwarding Service, you do **not** need native
tokens on the destination chain.
* [Deposited 10 USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
contract on Arc Testnet
* Created a TypeScript project and have `viem` installed
* You've set up a `.env` file with the following variables:
```text .env theme={null}
EVM_PRIVATE_KEY={YOUR_PRIVATE_KEY}
```
## Steps
Follow these steps to transfer a unified USDC balance using the Forwarding
Service. This example transfers 10 USDC from Arc Testnet to Base Sepolia. You
can adapt it for another
[supported chain](/gateway/references/supported-blockchains).
### Step 1. Create the transfer spec and estimate fees
Create a new file called `transfer.ts` in the root of your project and add the
following code to it. This code creates a
[transfer spec](/gateway/references/technical-guide#burn-intent) for 10 USDC on
Arc Testnet, then calls the
[`/estimate`](/api-reference/gateway/all/estimate-transfer) endpoint with
`enableForwarder=true` to determine the `maxFee` and `maxBlockHeight` values.
Using the estimate endpoint ensures the `maxFee` covers the gas fee, transfer
fee, and forwarding fee.
```ts transfer.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { pad, zeroAddress, formatUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet, baseSepolia } from "viem/chains";
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
const TRANSFER_VALUE = 10_000000n; // 10 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000; // 5 minutes
const sourceChain = {
name: "arcTestnet",
chain: arcTestnet,
usdcAddress: "0x3600000000000000000000000000000000000000",
domainId: 26,
};
const destinationChain = {
name: "baseSepolia",
chain: baseSepolia,
usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
domainId: 6,
};
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
console.log(`Using account: ${account.address}`);
console.log(`Transfer: ${sourceChain.name} → ${destinationChain.name}`);
console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log(`Forwarding: enabled\n`);
// Create the transfer spec
const spec = {
version: 1,
sourceDomain: sourceChain.domainId,
destinationDomain: destinationChain.domainId,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceChain.usdcAddress,
destinationToken: destinationChain.usdcAddress,
sourceDepositor: account.address,
destinationRecipient: account.address,
sourceSigner: account.address,
destinationCaller: zeroAddress,
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
const specBytes32 = {
...spec,
sourceContract: pad(spec.sourceContract.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationContract: pad(
spec.destinationContract.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceToken: pad(spec.sourceToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationToken: pad(spec.destinationToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
sourceDepositor: pad(spec.sourceDepositor.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationRecipient: pad(
spec.destinationRecipient.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceSigner: pad(spec.sourceSigner.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationCaller: pad(
spec.destinationCaller.toLowerCase() as `0x${string}`,
{ size: 32 },
),
};
// Estimate fees
console.log("Estimating fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([{ spec: specBytes32 }], (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = await estimateResponse.json();
const estimated = estimateResult.body[0].burnIntent;
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(` Forwarding fee: ${fees.forwardingFee} ${fees.token}`);
}
console.log(` Estimated maxFee: ${formatUnits(maxFee, 6)} ${fees.token}`);
```
**Note:** For production apps, verifying the balance on each chain before
creating burn intents is best practice. For this how-to, it's assumed that the
balance is created per the [prerequisites](#prerequisites). For a complete
end-to-end example that includes checking and error handling, see the Gateway
quickstarts ([EVM](/gateway/quickstarts/unified-balance-evm)).
### Step 2. Sign and submit the burn intent to the Gateway API
Add the following code to `transfer.ts`. This code constructs the
[EIP-712 typed data](/gateway/references/technical-guide#burn-intent), signs the
burn intent using the estimated `maxFee` and `maxBlockHeight`, and submits it to
the [`/transfer`](/api-reference/gateway/all/create-transfer-attestation)
endpoint with `enableForwarder=true`.
In forwarded flows, the `POST /v1/transfer` response may omit top-level
`attestation` and `signature` fields. Use the returned `transferId` to poll
`GET /v1/transfer/{id}` for the full transfer record.
```ts transfer.ts theme={null}
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: { maxBlockHeight, maxFee, spec: specBytes32 },
};
const signature = await account.signTypedData(
typedData as Parameters[0],
);
console.log("\nSigned burn intent.");
console.log("Submitting to Gateway API...");
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
[{ burnIntent: typedData.message, signature }],
(_key, value) => (typeof value === "bigint" ? value.toString() : value),
),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = await response.json();
const transferId = json.transferId;
if (!transferId) throw new Error("Missing transferId in response");
console.log(`Transfer ID: ${transferId}`);
```
### Step 3. Poll for transfer completion
Add the following code to `transfer.ts`. Because the Forwarding Service handles
the destination chain mint, you don't need to call the minter contract. Instead,
poll the [`/transfer/{id}`](/api-reference/gateway/all/get-transfer-by-id)
endpoint until the status reaches `confirmed` or `finalized`.
```ts transfer.ts theme={null}
console.log(`\nPolling for transfer completion...`);
const pollStart = Date.now();
let completed = false;
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
continue;
}
const details = await pollRes.json();
console.log(` Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
completed = true;
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
if (!completed) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`\nTransfer complete. ${formatUnits(TRANSFER_VALUE, 6)} USDC forwarded to ${destinationChain.name}.`,
);
```
### Step 4. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env transfer.ts
```
When the destination is Solana, you need to ensure that `destinationRecipient`
is an initialized USDC token account. If the recipient wallet address does not
already have a USDC token account, you can request
[automatic ATA creation](/gateway/references/forwarding-service#automatic-ata-creation-for-solana)
using the Forwarding Service.
This guide demonstrates how to forward from an EVM source blockchain to a Solana
destination with automatic Associated Token Account (ATA) creation.
## 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 the
[supported Testnets](/gateway/references/supported-blockchains#testnet) of
your choice to your wallet (this guide uses Arc Testnet)
* Funded your testnet wallet with native tokens on the source chain (this guide
uses Arc Testnet). With the Forwarding Service, you do **not** need native
tokens on the destination chain.
* [Deposited 10 USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
contract on Arc Testnet
* Created a TypeScript project and have `viem`, `@solana/web3.js`, and
`@solana/spl-token` installed
* You've set up a `.env` file with the following variables:
```text .env theme={null}
EVM_PRIVATE_KEY={YOUR_PRIVATE_KEY}
SOLANA_RECIPIENT_WALLET={RECIPIENT_SOLANA_WALLET_ADDRESS}
```
## Steps
Follow these steps to transfer a unified USDC balance to a Solana destination
using the Forwarding Service with automatic ATA creation. This example transfers
10 USDC from Arc Testnet to Solana Devnet.
### Step 1. Create the transfer spec and estimate fees
Create a new file called `transfer-solana.ts` in the root of your project and
add the following code to it. This code derives the recipient's ATA, creates a
transfer spec targeting Solana Devnet, and calls the
[`/estimate`](/api-reference/gateway/all/estimate-transfer) endpoint with
`enableForwarder=true` and `recipientSetupOptions` for automatic ATA creation.
The forwarding fee for Solana destinations includes the rent cost for ATA
creation when `recipientSetupOptions` is provided. If the recipient already
has an initialized USDC token account, omit `recipientSetupOptions` to avoid
the additional rent fee.
```ts transfer-solana.ts expandable highlight={4-5,15-20,28-33,66-67,73-82,86-87,101,151-154} theme={null}
import { randomBytes } from "node:crypto";
import { pad, zeroAddress, formatUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const TRANSFER_VALUE = 10_000000n; // 10 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000; // 5 minutes
const SOLANA_USDC_MINT = new PublicKey(
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
);
const SOLANA_GATEWAY_MINTER_ADDRESS = new PublicKey(
"GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr",
);
const sourceChain = {
name: "arcTestnet",
usdcAddress: "0x3600000000000000000000000000000000000000",
domainId: 26,
};
const destinationChain = {
name: "solanaDevnet",
minterAddress:
"0x" + Buffer.from(SOLANA_GATEWAY_MINTER_ADDRESS.toBytes()).toString("hex"),
usdcAddress: "0x" + Buffer.from(SOLANA_USDC_MINT.toBytes()).toString("hex"),
domainId: 5,
};
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
if (!process.env.SOLANA_RECIPIENT_WALLET)
throw new Error("SOLANA_RECIPIENT_WALLET not set");
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
// Derive the recipient's USDC ATA from their wallet address
const recipientWallet = new PublicKey(process.env.SOLANA_RECIPIENT_WALLET);
const recipientAta = getAssociatedTokenAddressSync(
SOLANA_USDC_MINT,
recipientWallet,
);
const recipientAtaHex =
"0x" + Buffer.from(recipientAta.toBytes()).toString("hex");
const recipientWalletHex =
"0x" + Buffer.from(recipientWallet.toBytes()).toString("hex");
console.log(`Using account: ${account.address}`);
console.log(`Transfer: ${sourceChain.name} → ${destinationChain.name}`);
console.log(`Recipient wallet: ${recipientWallet.toBase58()}`);
console.log(`Recipient ATA: ${recipientAta.toBase58()}`);
console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log(`Forwarding: enabled\n`);
// Create the transfer spec
const spec = {
version: 1,
sourceDomain: sourceChain.domainId,
destinationDomain: destinationChain.domainId,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: destinationChain.minterAddress,
sourceToken: sourceChain.usdcAddress,
destinationToken: destinationChain.usdcAddress,
sourceDepositor: account.address,
destinationRecipient: recipientAtaHex,
sourceSigner: account.address,
destinationCaller: zeroAddress,
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
const specBytes32 = {
...spec,
sourceContract: pad(spec.sourceContract.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationContract: pad(
spec.destinationContract.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceToken: pad(spec.sourceToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationToken: pad(spec.destinationToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
sourceDepositor: pad(spec.sourceDepositor.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationRecipient: pad(
spec.destinationRecipient.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceSigner: pad(spec.sourceSigner.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationCaller: pad(
spec.destinationCaller.toLowerCase() as `0x${string}`,
{ size: 32 },
),
};
// Estimate fees with recipientSetupOptions for automatic ATA creation
console.log("Estimating fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
[
{
spec: specBytes32,
recipientSetupOptions: {
includeRecipientSetup: true,
recipientOwnerAddress: recipientWalletHex,
},
},
],
(_key, value) => (typeof value === "bigint" ? value.toString() : value),
),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = await estimateResponse.json();
const estimated = estimateResult.body[0].burnIntent;
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(` Forwarding fee: ${fees.forwardingFee} ${fees.token}`);
}
console.log(` Estimated maxFee: ${formatUnits(maxFee, 6)} ${fees.token}`);
```
### Step 2. Sign and submit the burn intent to the Gateway API
Add the following code to `transfer-solana.ts`. The signing step is the same as
the EVM flow, but the request body includes `recipientSetupOptions` so the
Forwarding Service creates the ATA on Solana.
The `recipientOwnerAddress` is the recipient's Solana wallet address in bytes32
hex format. The API validates that the `destinationRecipient` in the transfer
spec matches the canonical ATA derived from this address and the USDC token
mint. If they don't match, the request is rejected.
In forwarded flows, the `POST /v1/transfer` response may omit top-level
`attestation` and `signature` fields. Use the returned `transferId` to poll
`GET /v1/transfer/{id}` for the full transfer record.
```ts transfer-solana.ts highlight={24-27} theme={null}
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: { maxBlockHeight, maxFee, spec: specBytes32 },
};
const signature = await account.signTypedData(
typedData as Parameters[0],
);
console.log("\nSigned burn intent.");
console.log("Submitting to Gateway API...");
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
[
{
burnIntent: {
...typedData.message,
recipientSetupOptions: {
includeRecipientSetup: true,
recipientOwnerAddress: recipientWalletHex,
},
},
signature,
},
],
(_key, value) => (typeof value === "bigint" ? value.toString() : value),
),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = await response.json();
const transferId = json.transferId;
if (!transferId) throw new Error("Missing transferId in response");
console.log(`Transfer ID: ${transferId}`);
```
### Step 3. Poll for transfer completion
Add the following code to `transfer-solana.ts`. Because the Forwarding Service
handles the destination chain mint, you don't need to call the minter contract.
Instead, poll the
[`/transfer/{id}`](/api-reference/gateway/all/get-transfer-by-id) endpoint until
the status reaches `confirmed` or `finalized`.
```ts transfer-solana.ts theme={null}
console.log(`\nPolling for transfer completion...`);
const pollStart = Date.now();
let completed = false;
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
continue;
}
const details = await pollRes.json();
console.log(` Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
completed = true;
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
if (!completed) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`\nTransfer complete. ${formatUnits(TRANSFER_VALUE, 6)} USDC forwarded to ${destinationChain.name}.`,
);
```
### Step 4. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env transfer-solana.ts
```
# How-to: Manage delegates for Gateway transfers
Source: https://developers.circle.com/gateway/howtos/manage-delegates
Add or remove delegate accounts that can authorize Gateway transfers on behalf of a depositor.
Gateway lets you delegate your deposit so that another account can authorize
transfers on your behalf. Adding a delegate grants that account a full allowance
to sign burn intents for your deposited USDC, which is useful when you want a
separate account to initiate transfers, or when you'd rather sign with a
different account than the one that holds the deposit.
A delegate can be any account whose signatures Gateway accepts, such as an
Externally Owned Account (EOA) or a smart contract using
[ERC-1271](/gateway/references/erc-1271).
A smart contract account (SCA) can also authorize transfers directly through
[ERC-1271](/gateway/references/erc-1271), without adding a delegate at all.
See [How-to: Transfer USDC from a smart contract
account](/gateway/howtos/transfer-with-erc-1271).
This guide demonstrates how to add and remove delegates for Gateway transfers
using Circle [developer-controlled wallets](/wallets/dev-controlled).
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Created a [Circle Console](https://console.circle.com) account
* Obtained an API key and
[registered your Entity Secret](/wallets/dev-controlled/register-entity-secret)
* Created two wallets via Circle developer-controlled wallets:
* The depositor wallet holds your Gateway USDC deposits
* The delegate wallet will sign burn intents on behalf of the depositor
* Deposited USDC into the Gateway Wallet from your depositor wallet
* Created a TypeScript project with the
[Developer-Controlled Wallets SDK](/sdks/developer-controlled-wallets-nodejs-sdk)
installed
* Set up a `.env` file with the following variables:
```text .env theme={null}
CIRCLE_API_KEY={YOUR_API_KEY}
CIRCLE_ENTITY_SECRET={YOUR_ENTITY_SECRET}
DEPOSITOR_ADDRESS={YOUR_DEPOSITOR_WALLET_ADDRESS}
DELEGATE_WALLET_ADDRESS={YOUR_DELEGATE_WALLET_ADDRESS}
```
## Add a delegate
Follow these steps to authorize a delegate to sign burn intents on behalf of
your depositor wallet.
### Step 1. Create the add delegate script
Create a new file called `add-delegate.ts` in the root of your project and add
the following code. This calls the
[`addDelegate()`](/gateway/references/contract-interfaces-and-events#adddelegate)
method on the Gateway Wallet contract on Arc Testnet.
```ts add-delegate.ts theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
const GATEWAY_WALLET = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; // Arc Testnet
const BLOCKCHAIN = "ARC-TESTNET";
const client = initiateDeveloperControlledWalletsClient({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
async function waitForTx(txId: string) {
while (true) {
const { data } = await client.getTransaction({ id: txId });
const state = data?.transaction?.state;
if (["COMPLETE", "CONFIRMED"].includes(state!)) return;
if (["FAILED", "DENIED", "CANCELLED"].includes(state!))
throw new Error(`Failed: ${state}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
async function main() {
console.log(`\n=== Processing ${BLOCKCHAIN} ===`);
console.log(`Adding delegate: ${process.env.DELEGATE_WALLET_ADDRESS}`);
const tx = await client.createContractExecutionTransaction({
walletAddress: process.env.DEPOSITOR_ADDRESS!,
blockchain: BLOCKCHAIN,
contractAddress: GATEWAY_WALLET,
abiFunctionSignature: "addDelegate(address,address)",
abiParameters: [USDC_ADDRESS, process.env.DELEGATE_WALLET_ADDRESS!],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
await waitForTx(tx.data?.id!);
console.log(`Done on ${BLOCKCHAIN}`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### Step 2. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env add-delegate.ts
```
The script output shows "Done on \" when the delegate is
successfully authorized. You can now use the delegate to sign burn intents on
behalf of your depositor wallet when making Gateway transfers.
### Step 3. Repeat for other blockchains
To add the delegate on additional blockchains where the depositor holds Gateway
deposits, repeat Steps 1-2 with different `BLOCKCHAIN` and `USDC_ADDRESS` values
for your target blockchains. Check the
[USDC contract addresses](/stablecoins/usdc-contract-addresses) for the correct
values.
The delegate authorization remains valid until explicitly removed with the
[`removeDelegate()`](/gateway/references/contract-interfaces-and-events#removedelegate)
method.
## Remove a delegate
Follow these steps to revoke a delegate's authorization to sign burn intents on
behalf of your depositor wallet. After removing a delegate, note that:
* The delegate can no longer create new burn intents for the depositor wallet
* Burn intents that were already signed by the delegate remain valid and can
still be executed onchain
* This ensures that burns may be executed safely even in the event of a
revocation
* The Gateway API reflects revocations once they are finalized onchain
This behavior is by design to prevent race conditions where a valid burn intent
becomes unusable mid-flight.
### Step 1. Create the remove delegate script
Create a new file called `remove-delegate.ts` in the root of your project and
add the following code. This is nearly identical to the add delegate script, but
calls
[`removeDelegate()`](/gateway/references/contract-interfaces-and-events#removedelegate)
instead.
```ts remove-delegate.ts theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
const GATEWAY_WALLET = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const USDC_ADDRESS = "0x3600000000000000000000000000000000000000"; // Arc Testnet
const BLOCKCHAIN = "ARC-TESTNET";
const client = initiateDeveloperControlledWalletsClient({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
async function waitForTx(txId: string) {
while (true) {
const { data } = await client.getTransaction({ id: txId });
const state = data?.transaction?.state;
if (["COMPLETE", "CONFIRMED"].includes(state!)) return;
if (["FAILED", "DENIED", "CANCELLED"].includes(state!))
throw new Error(`Failed: ${state}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
async function main() {
console.log(`\n=== Processing ${BLOCKCHAIN} ===`);
console.log(`Removing delegate: ${process.env.DELEGATE_WALLET_ADDRESS}`);
const tx = await client.createContractExecutionTransaction({
walletAddress: process.env.DEPOSITOR_ADDRESS!,
blockchain: BLOCKCHAIN,
contractAddress: GATEWAY_WALLET,
abiFunctionSignature: "removeDelegate(address,address)",
abiParameters: [USDC_ADDRESS, process.env.DELEGATE_WALLET_ADDRESS!],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
await waitForTx(tx.data?.id!);
console.log(`Done on ${BLOCKCHAIN}`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### Step 2. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env remove-delegate.ts
```
# How-to: Transfer a unified USDC balance instantly
Source: https://developers.circle.com/gateway/howtos/transfer-unified-usdc-balance
Once you have established a unified USDC balance, you can transfer it instantly
to any supported destination chain.
This guide demonstrates how to transfer your unified balance with an EOA
signature. To transfer from a smart contract account with ERC-1271 instead,
follow
[Transfer USDC from a smart contract account](/gateway/howtos/transfer-with-erc-1271).
Select a tab below for EVM or Solana-specific instructions.
## 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 the
[supported Testnets](/gateway/references/supported-blockchains#testnet) of
your choice to your wallet (this guide uses Arc Testnet, Avalanche Fuji and
Sei Testnet)
* Funded your testnet wallet with native tokens on the destination chain (this
guide uses Sei Testnet)
* [Deposited 10 USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
contracts on Arc Testnet and Avalanche Fuji (creating a unified balance of 20
USDC)
* Created a TypeScript project and have `viem` installed
* You've set up a `.env` file with the following variables:
```text .env theme={null}
EVM_PRIVATE_KEY={YOUR_PRIVATE_KEY}
```
## Steps
Follow these steps to transfer a unified USDC balance. This example uses a
unified balance split between Arc Testnet and Avalanche Fuji. You can adapt it
for any blockchains where you hold a unified balance.
### Step 1. Create and sign burn intents for the source chains
Create a new file called `transfer.ts` in the root of your project and add the
following code to it. This code creates and signs
[burn intents](/gateway/references/technical-guide#burn-intent) for 5 USDC on
Arc Testnet and 5 USDC on Avalanche Fuji.
```ts transfer.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
http,
maxUint256,
zeroAddress,
pad,
createPublicClient,
getContract,
createWalletClient,
formatUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { avalancheFuji, arcTestnet, seiTestnet } from "viem/chains";
/* Constants */
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
const TRANSFER_VALUE = 5_000000n; // 5 USDC (6 decimals)
const MAX_FEE = 2_010000n;
// Source chains configuration
const sourceChains = [
{
name: "arcTestnet",
chain: arcTestnet,
usdcAddress: "0x3600000000000000000000000000000000000000",
domainId: 26,
},
{
name: "avalancheFuji",
chain: avalancheFuji,
usdcAddress: "0x5425890298aed601595a70ab815c96711a31bc65",
domainId: 1,
},
];
// Destination chain configuration
const destinationChain = {
name: "seiTestnet",
chain: seiTestnet,
usdcAddress: "0x4fCF1784B31630811181f670Aea7A7bEF803eaED",
domainId: 16,
};
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
const gatewayMinterAbi = [
{
type: "function",
name: "gatewayMint",
inputs: [
{ name: "attestationPayload", type: "bytes" },
{ name: "signature", type: "bytes" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
// Get account from environment
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
console.log(`Using account: ${account.address}`);
console.log(`Transferring from: ${sourceChains.map((c) => c.name).join(", ")}`);
console.log(`Transferring to: ${destinationChain.name}\n`);
// Create and sign burn intents for each source chain
const requests = [];
for (const sourceChain of sourceChains) {
console.log(
`Creating burn intent from ${sourceChain.name} → ${destinationChain.name}...`,
);
const burnIntent = {
maxBlockHeight: maxUint256,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: sourceChain.domainId,
destinationDomain: destinationChain.domainId,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceChain.usdcAddress,
destinationToken: destinationChain.usdcAddress,
sourceDepositor: account.address,
destinationRecipient: account.address,
sourceSigner: account.address,
destinationCaller: zeroAddress,
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: {
...burnIntent,
spec: {
...burnIntent.spec,
sourceContract: pad(
burnIntent.spec.sourceContract.toLowerCase() as `0x${string}`,
{ size: 32 },
),
destinationContract: pad(
burnIntent.spec.destinationContract.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceToken: pad(
burnIntent.spec.sourceToken.toLowerCase() as `0x${string}`,
{ size: 32 },
),
destinationToken: pad(
burnIntent.spec.destinationToken.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceDepositor: pad(
burnIntent.spec.sourceDepositor.toLowerCase() as `0x${string}`,
{ size: 32 },
),
destinationRecipient: pad(
burnIntent.spec.destinationRecipient.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceSigner: pad(
burnIntent.spec.sourceSigner.toLowerCase() as `0x${string}`,
{ size: 32 },
),
destinationCaller: pad(
burnIntent.spec.destinationCaller.toLowerCase() as `0x${string}`,
{ size: 32 },
),
},
},
};
const signature = await account.signTypedData(
typedData as Parameters[0],
);
requests.push({ burnIntent: typedData.message, signature });
}
console.log("Signed burn intents.\n");
```
**Note:** For production apps, verifying the balance on each chain before
creating burn intents is best practice. For this how-to, it's assumed that the
balances are created per the [prerequisites](#prerequisites). For a complete
end-to-end example that includes checking and error handling, see the Gateway
quickstarts ([EVM](/gateway/quickstarts/unified-balance-evm),
[Solana](/gateway/quickstarts/unified-balance-solana)).
### Step 2. Submit the burn intents to the Gateway API to obtain an attestation
Add the following code to `transfer.ts`. This code constructs a Gateway API
request to the
[`/transfer`](/api-reference/gateway/all/create-transfer-attestation) endpoint
and obtains the attestation from that endpoint.
```ts transfer.ts theme={null}
console.log("Submitting to Gateway API...");
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requests, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = await response.json();
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature } = json;
```
### Step 3. Transfer USDC to the destination chain
Add the following code to `transfer.ts`. This code performs a call to the
[Gateway Minter contract](/gateway/references/contract-interfaces-and-events#gatewayminter)
on Sei Testnet to instantly mint the USDC to your account on that chain.
```ts transfer.ts theme={null}
console.log(`\nMinting funds on ${destinationChain.chain.name}...`);
const publicClient = createPublicClient({
chain: destinationChain.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: destinationChain.chain,
transport: http(),
});
const gatewayMinter = getContract({
address: GATEWAY_MINTER_ADDRESS,
abi: gatewayMinterAbi,
client: walletClient,
});
const mintTx = await gatewayMinter.write.gatewayMint([attestation, signature], {
account,
});
await publicClient.waitForTransactionReceipt({ hash: mintTx });
const totalMinted = BigInt(requests.length) * TRANSFER_VALUE;
console.log(`\nMinted ${formatUnits(totalMinted, 6)} USDC`);
console.log(`Mint transaction hash:`, mintTx);
```
### Step 4. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env transfer.ts
```
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Prepared Solana Devnet wallets (sender and recipient) and have the private key
pairs exported as JSON arrays
* Funded your testnet wallet with USDC and native tokens
* [Deposited 10 USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
contract on Solana Devnet
* Created a TypeScript project and have the following dependencies installed:
* `@solana/web3.js`
* `@solana/spl-token`
* `@coral-xyz/anchor`
* `@solana/buffer-layout`
* `bn.js`
* `bs58`
* You've set up a `.env` file with the following variable:
```text .env theme={null}
SOLANA_PRIVATE_KEYPAIR={YOUR_SOLANA_KEYPAIR_ARRAY}
RECIPIENT_KEYPAIR={YOUR_RECIPIENT_KEYPAIR_ARRAY}
```
To generate a new Solana keypair, run:
```shell theme={null}
solana-keygen new -o keypair.json --no-bip39-passphrase
```
Copy the byte array from the JSON file to your `.env` file.
If your wallet exports a private key hash instead, you can use `bs58` to convert
it:
```ts TypeScript theme={null}
const bytes = bs58.decode({ YOUR_PRIVATE_KEY_HASH });
console.log(JSON.stringify(Array.from(bytes)));
```
## Steps
Follow these steps to transfer a unified USDC balance from Solana to another
Solana account. You can adapt this example for crosschain transfers.
### Step 1. Set up your Solana client and helpers
Create a new file called `transfer.ts` in the root of your project and add the
following code. This code imports required dependencies and defines constants
for the Solana connection, Gateway addresses, and transfer parameters.
```ts transfer.ts theme={null}
import { randomBytes } from "node:crypto";
import * as crypto from "crypto";
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
} from "@solana/spl-token";
import {
u32be,
nu64be,
struct,
seq,
blob,
offset,
Layout,
} from "@solana/buffer-layout";
import bs58 from "bs58";
/* Constants */
const RPC_ENDPOINT = "https://api.devnet.solana.com";
const GATEWAY_WALLET_ADDRESS = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const GATEWAY_MINTER_ADDRESS = "GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr";
const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_ZERO_ADDRESS = "11111111111111111111111111111111";
const SOLANA_DOMAIN = 5;
const TRANSFER_AMOUNT = 2; // 2 USDC
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
```
### Step 2. Set up buffer layouts and IDL
Add the following code to `transfer.ts` to define custom layout classes for
serialization, structures for encoding burn intents and decoding attestations,
and the Gateway Minter IDL. Solana doesn't support EIP-712 typed data, so burn
intents must be encoded manually using buffer layouts.
```ts transfer.ts expandable theme={null}
// Custom layout for Solana PublicKey (32 bytes)
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
// Type 'as any' used due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
// Sample-local IDL subset for this example.
const gatewayMinterIdl = {
address: GATEWAY_MINTER_ADDRESS,
metadata: { name: "gatewayMinter", version: "0.1.0", spec: "0.1.0" },
instructions: [
{
name: "gatewayMint",
discriminator: [12, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "destinationCaller", signer: true },
{ name: "gatewayMinter" },
{ name: "systemProgram" },
{ name: "tokenProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [
{ name: "params", type: { defined: { name: "gatewayMintParams" } } },
],
},
],
types: [
{
name: "gatewayMintParams",
type: {
kind: "struct",
fields: [
{ name: "attestation", type: "bytes" },
{ name: "signature", type: "bytes" },
],
},
},
],
};
```
### Step 3. Define helper functions
Add the following code to `transfer.ts` to define helper functions for creating,
encoding, and signing burn intents, converting addresses between formats,
decoding attestations from the Gateway API, and finding Program Derived
Addresses (PDAs). The signing process uses Ed25519 with a specific prefix
required by the Gateway Wallet program.
```ts transfer.ts expandable theme={null}
function createBurnIntent(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(USDC_ADDRESS),
destinationToken: addressToBytes32(USDC_ADDRESS),
sourceDepositor: addressToBytes32(sourceDepositor),
destinationRecipient: addressToBytes32(destinationRecipient),
sourceSigner: addressToBytes32(sourceSigner),
destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
function encodeBurnIntent(bi: any): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function signBurnIntent(keypair: Keypair, payload: any): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
function addressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
function decodeAttestationSet(attestation: string) {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as {
attestations: Array<{
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
}>;
};
}
function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
```
### Step 4. Set up the environment and create the burn intent
Add the following code to `transfer.ts` to load keypairs from environment
variables, create a Solana connection, set up the recipient's Associated Token
Account, and create and sign the burn intent.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
```ts transfer.ts theme={null}
if (!process.env.SOLANA_PRIVATE_KEYPAIR)
throw new Error("SOLANA_PRIVATE_KEYPAIR not set");
if (!process.env.RECIPIENT_KEYPAIR)
throw new Error("RECIPIENT_KEYPAIR not set");
const senderKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.SOLANA_PRIVATE_KEYPAIR)),
);
const recipientKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.RECIPIENT_KEYPAIR)),
);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
console.log(`Using account: ${senderKeypair.publicKey.toBase58()}`);
console.log(`Transferring from: Solana Devnet → Solana Devnet\n`);
// Create recipient's Associated Token Account
const recipientAta = getAssociatedTokenAddressSync(
usdcMint,
recipientKeypair.publicKey,
);
console.log("Creating recipient's Associated Token Account...");
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
senderKeypair.publicKey,
recipientAta,
recipientKeypair.publicKey,
usdcMint,
);
const tx = new Transaction().add(createAtaIx);
await sendAndConfirmTransaction(connection, tx, [senderKeypair]);
console.log(`Recipient ATA: ${recipientAta.toBase58()}\n`);
// Create and sign burn intent
console.log(`Creating burn intent from Solana Devnet → Solana Devnet...`);
const burnIntent = createBurnIntent({
sourceDepositor: senderKeypair.publicKey.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: senderKeypair.publicKey.toBase58(),
});
const burnIntentSignature = signBurnIntent(senderKeypair, burnIntent);
const request = [{ burnIntent, signature: burnIntentSignature }];
console.log("Signed burn intent.\n");
```
### Step 5. Submit to Gateway API and decode the attestation
Add the following code to `transfer.ts` to submit the burn intent to the Gateway
API, obtain an attestation, and decode the attestation set for use in the mint
operation.
```ts transfer.ts theme={null}
console.log("Submitting to Gateway API...");
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
const json = await response.json();
if (json.success === false) {
throw new Error(`Gateway API error: ${json.message}`);
}
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
const decoded = decodeAttestationSet(attestation);
```
### Step 6. Mint USDC on the destination chain
Add the following code to `transfer.ts` to set up the Anchor program, prepare
the required accounts, and call the Gateway Minter to mint USDC on Solana Devnet
using the attestation from the previous step.
The ordered remaining-account list and PDA derivations are documented in
[Solana Programs and
Interfaces](/gateway/references/solana-programs#gatewaymint). For static
instruction definitions, use the onchain IDLs linked from that page.
```ts transfer.ts theme={null}
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const anchorWallet = new Wallet(senderKeypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
// Mint on Solana
const remainingAccounts = decoded.attestations.flatMap((e) => [
{
pubkey: findCustodyPda(e.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{ pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
{
pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
isWritable: true,
isSigner: false,
},
]);
const attestationBytes = Buffer.from(attestation.slice(2), "hex");
const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");
console.log("\nMinting funds on Solana Devnet...");
const mintTx = await minterProgram.methods
.gatewayMint({ attestation: attestationBytes, signature: signatureBytes })
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: senderKeypair.publicKey,
payer: senderKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.signers([senderKeypair])
.rpc();
// Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction(
{
signature: mintTx,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed",
);
console.log(`\nMinted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash:`, mintTx);
```
### Step 7. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env transfer.ts
```
# How-to: Transfer USDC from a smart contract account
Source: https://developers.circle.com/gateway/howtos/transfer-with-erc-1271
Sign a Gateway burn intent with a Circle smart contract account and transfer a unified USDC balance using ERC-1271 programmable authorization.
Once you have established a unified USDC balance in a smart contract account
(SCA), you can transfer it instantly to any supported destination chain using
[ERC-1271](/gateway/references/erc-1271). The SCA authorizes the transfer with
its own signature—no delegate Externally Owned Account (EOA) required.
This guide demonstrates how to transfer your unified balance with a Circle
[Smart Contract Account (SCA) wallet](/wallets/account-types#smart-contract-accounts-sca).
For the EOA path, see
[Transfer a unified USDC balance](/gateway/howtos/transfer-unified-usdc-balance).
This example uses Circle SCA wallets but the Gateway request is the same for any
ERC-1271 wallet. Only signing differs: obtain opaque signature bytes that make
`isValidSignature` return `0x1626ba7e`, then POST
`{ burnIntent, signature, contractSigner: true }`. Do not split the signature
into `v`, `r`, `s`.
Nanopayments and x402 batch settlement do not support ERC-1271. See
[ERC-1271 programmable authorization](/gateway/references/erc-1271).
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Created a [Circle Console](https://console.circle.com) account
* Obtained an API key and
[registered your Entity Secret](/wallets/dev-controlled/register-entity-secret)
* Created SCA wallets via Circle
[developer-controlled wallets](/wallets/dev-controlled) on the source and
destination chains (this guide uses Arc Testnet and Base Sepolia)
* The source SCA holds your Gateway USDC deposits and signs burn intents
* The destination wallet submits the mint (can be the same address on another
blockchain)
* [Deposited USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
from your SCA on Arc Testnet and waited for the deposit to finalize
* Created a TypeScript project with the
[Developer-Controlled Wallets SDK](/sdks/developer-controlled-wallets-nodejs-sdk)
installed
* Set up a `.env` file with the following variables:
```text .env theme={null}
CIRCLE_API_KEY={YOUR_API_KEY}
CIRCLE_ENTITY_SECRET={YOUR_ENTITY_SECRET}
DEPOSITOR_ADDRESS={YOUR_SCA_WALLET_ADDRESS}
RECIPIENT_ADDRESS={YOUR_DESTINATION_WALLET_ADDRESS}
```
## Steps
Follow these steps to transfer a unified USDC balance from an SCA. This example
transfers 1 USDC from Arc Testnet to Base Sepolia. You can adapt it for any
blockchains where you hold a unified balance.
### Step 1. Create and sign the burn intent with the SCA
Create a new file called `transfer.ts` in the root of your project and add the
following code to it. This code creates a
[burn intent](/gateway/references/technical-guide#burn-intent) for 1 USDC on Arc
Testnet and signs it with your SCA. Set `sourceDepositor` and `sourceSigner` to
the SCA address.
```typescript transfer.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();
const SOURCE_CHAIN = "ARC-TESTNET";
const DEST_CHAIN = "BASE-SEPOLIA";
const sourceConfig = {
chainName: "Arc Testnet",
usdc: "0x3600000000000000000000000000000000000000",
domain: 26,
walletChain: SOURCE_CHAIN,
};
const destinationConfig = {
chainName: "Base Sepolia",
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
domain: 6,
walletChain: DEST_CHAIN,
};
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
];
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
];
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
];
function addressToBytes32(address: string) {
return ("0x" +
address
.toLowerCase()
.replace(/^0x/, "")
.padStart(64, "0")) as `0x${string}`;
}
function stringifyTypedData(obj: T) {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
async function waitForTx(
client: ReturnType,
txId: string,
) {
while (true) {
const { data } = await client.getTransaction({ id: txId });
const state = data?.transaction?.state;
if (["COMPLETE", "CONFIRMED"].includes(state!)) return;
if (["FAILED", "DENIED", "CANCELLED"].includes(state!))
throw new Error(`Failed: ${state}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
function createBurnIntent(params: {
depositorAddress: string;
recipientAddress?: string;
}) {
const { depositorAddress, recipientAddress = depositorAddress } = params;
return {
maxBlockHeight: MAX_UINT256_DEC,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: sourceConfig.domain,
destinationDomain: destinationConfig.domain,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceConfig.usdc,
destinationToken: destinationConfig.usdc,
sourceDepositor: depositorAddress,
destinationRecipient: recipientAddress,
sourceSigner: depositorAddress,
destinationCaller: "0x0000000000000000000000000000000000000000",
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
},
};
}
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent",
message: {
...burnIntent,
spec: {
...burnIntent.spec,
sourceContract: addressToBytes32(burnIntent.spec.sourceContract),
destinationContract: addressToBytes32(
burnIntent.spec.destinationContract,
),
sourceToken: addressToBytes32(burnIntent.spec.sourceToken),
destinationToken: addressToBytes32(burnIntent.spec.destinationToken),
sourceDepositor: addressToBytes32(burnIntent.spec.sourceDepositor),
destinationRecipient: addressToBytes32(
burnIntent.spec.destinationRecipient,
),
sourceSigner: addressToBytes32(burnIntent.spec.sourceSigner),
destinationCaller: addressToBytes32(burnIntent.spec.destinationCaller),
},
},
};
}
const client = initiateDeveloperControlledWalletsClient({
apiKey: process.env.CIRCLE_API_KEY!,
entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});
const depositorAddress = process.env.DEPOSITOR_ADDRESS!;
const recipientAddress = process.env.RECIPIENT_ADDRESS!;
console.log(
`Transferring 1 USDC from ${sourceConfig.chainName} to ${destinationConfig.chainName}`,
);
console.log(`Depositor / signer (SCA): ${depositorAddress}`);
console.log(`Recipient: ${recipientAddress}`);
const burnIntent = createBurnIntent({
depositorAddress,
recipientAddress,
});
const typedData = burnIntentTypedData(burnIntent);
const sigResp = await client.signTypedData({
walletAddress: depositorAddress,
blockchain: sourceConfig.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) throw new Error("Failed to sign burn intent");
```
For production apps, verifying the balance on each blockchain before creating
burn intents is best practice. For this how-to, it's assumed that the balances
are created per the [prerequisites](#prerequisites). For a complete end-to-end
example that includes checking and error handling, see the Gateway quickstarts
([EVM](/gateway/quickstarts/unified-balance-evm),
[Solana](/gateway/quickstarts/unified-balance-solana)).
### Step 2. Submit the burn intent to the Gateway API to obtain an attestation
Add the following code to `transfer.ts`. This code constructs a Gateway API
request to the
[`/transfer`](/api-reference/gateway/all/create-transfer-attestation) endpoint
with `contractSigner: true` so Gateway validates the signature with ERC-1271,
and obtains the attestation from that endpoint.
```typescript transfer.ts theme={null}
const requests = [
{
burnIntent: typedData.message,
signature,
contractSigner: true,
},
];
console.log("Submitting to Gateway API...");
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(requests),
});
if (!response.ok) {
throw new Error(await response.text());
}
const { attestation, signature: operatorSig } = await response.json();
if (!attestation || !operatorSig) {
throw new Error("Invalid Gateway API response");
}
```
When `contractSigner` is `true`, Gateway validates via ERC-1271 on
`sourceSigner`. When omitted or `false`, it expects an EOA ECDSA signature. See
[Use ERC-1271 validation](/gateway/references/erc-1271#opting-into-erc-1271-validation).
### Step 3. Transfer USDC to the destination chain
Add the following code to `transfer.ts`. This code performs a call to the
[Gateway Minter contract](/gateway/references/contract-interfaces-and-events#gatewayminter)
on Base Sepolia to instantly mint the USDC to your recipient wallet on that
blockchain.
```typescript transfer.ts theme={null}
console.log(`\nMinting funds on ${destinationConfig.chainName}...`);
const tx = await client.createContractExecutionTransaction({
walletAddress: recipientAddress,
blockchain: destinationConfig.walletChain,
contractAddress: GATEWAY_MINTER_ADDRESS,
abiFunctionSignature: "gatewayMint(bytes,bytes)",
abiParameters: [attestation, operatorSig],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const txId = tx.data?.id;
if (!txId) throw new Error("Failed to submit mint transaction");
await waitForTx(client, txId);
console.log(`\nMinted 1 USDC on ${destinationConfig.chainName}`);
console.log(`Mint transaction ID:`, txId);
```
`RECIPIENT_ADDRESS` must be a destination chain developer-controlled wallet in
your Circle account. The script uses `createContractExecutionTransaction()` to
submit `gatewayMint()`.
### Step 4. Run the script
Run the script with the following command:
```shell theme={null}
node --env-file=.env transfer.ts
```
ERC-1271 validation runs offchain as a read-only simulation. Authorization
logic that modifies onchain state during validation isn't supported. See
[ERC-1271 programmable
authorization](/gateway/references/erc-1271#limitations-and-considerations)
for the full list of limitations.
# Nanopayments
Source: https://developers.circle.com/gateway/nanopayments
Gas-free USDC nanopayments down to $0.000001, powered by Circle Gateway batched settlement
Circle Gateway enables gas-free USDC nanopayments by batching thousands of
payments into a single onchain transaction. Instead of settling each payment
individually, buyers sign offchain authorizations and Gateway settles net
positions in bulk, eliminating per-transaction gas costs and making sub-cent
payments economically viable. Nanopayments power agentic commerce by giving
developers and AI agents a financial rail purpose-built for high-frequency
agentic payments at scale.
Nanopayments and x402 batch settlement require EOA signatures and do not support
ERC-1271. For standard Gateway transfers with smart contracts or smart contract
wallets, see
[ERC-1271 programmable authorization](/gateway/references/erc-1271).
## Key features
Buyers sign payment authorizations offchain at zero gas cost. Gateway
settles in bulk, so neither party pays per-transaction fees.
Send as little as \$0.000001 USDC per payment. Batched settlement keeps fees
from exceeding the payment itself.
Sellers receive payments in their Gateway balance and can withdraw to any
supported blockchain.
## What you can build
Enable AI agents to pay autonomously for compute, data, memory, and services.
Agents transact at high frequency and extreme granularity, executing thousands
of sub-cent payments per minute without gas friction. For the agent-builder
workflow, see [Agent nanopayments](/agent-stack/agent-nanopayments).
Charge per API call, per second of compute, or per dataset access. With
transaction costs near zero, fine-grained billing models become practical for
the first time.
Build decentralized marketplaces where services, models, and data is priced and
traded in real time at sub-cent granularity.
Implement pay-per-second content, micro-rewards, and continuous value flows
where traditional payment rails are too expensive to operate.
## How it works
Nanopayments enables the [x402 protocol](/gateway/nanopayments/concepts/x402),
an open standard built on the HTTP `402 Payment Required` status code, by using
Circle Gateway's
[batched settlement](/gateway/nanopayments/concepts/batched-settlement)
infrastructure. Instead of settling each x402 payment individually onchain,
Gateway aggregates signed payment authorizations and settles net positions in
bulk. This is what makes sub-cent x402 payments economically viable.
The end-to-end flow is:
1. A buyer deposits USDC into a Gateway Wallet contract (one-time onchain
transaction).
2. A buyer requests a paid resource from a seller's API.
3. The seller responds with `402 Payment Required` and payment details.
4. The buyer signs an EIP-3009 payment authorization (offchain, zero gas).
5. The buyer retries the request with the signed authorization attached.
6. The seller verifies the signature and serves the resource immediately.
7. Gateway collects authorizations and settles them in batches onchain,
crediting the seller's Gateway balance.
## Get started
Deposit USDC, pay for an x402-protected resource without gas fees, and check
your balance
Add payment middleware to your Express API and start accepting gasless USDC
payments
Learn about the open payment protocol that powers Nanopayments
Understand how Gateway aggregates payments and settles them onchain
Explore the API endpoints for Nanopayments
# How batched settlement works
Source: https://developers.circle.com/gateway/nanopayments/concepts/batched-settlement
How Circle Gateway aggregates payment authorizations and settles them onchain in a single transaction
Circle Gateway enables nanopayments by batching many individual payment
authorizations into a single onchain transaction. Instead of settling each
payment separately (and paying gas each time), Gateway collects signed
authorizations offchain, computes net balance changes, and applies them in bulk.
This document explains the batching lifecycle, the security model that keeps it
non-custodial, and how balances move through the system.
## The economics of batching
Settling a USDC transfer onchain costs gas. Even on low-fee blockchains, gas
costs can exceed the value of a sub-cent payment. Batching solves this by
amortizing gas across thousands of payments:
| Approach | Gas cost per payment | Viable minimum payment |
| --------------------- | --------------------------------- | --------------------------------- |
| Individual settlement | Full gas per transaction | \~\$0.01+ (depends on blockchain) |
| Batched settlement | Gas / number of payments in batch | \$0.000001 |
By settling net positions rather than individual transfers, Gateway reduces both
the number of onchain transactions and the total gas consumed.
## Payment lifecycle
A nanopayment moves through five stages from initiation to settlement:
```mermaid theme={null}
sequenceDiagram
participant Buyer
participant Seller
participant Gateway as Circle Gateway
Buyer->>Gateway: 1. Deposit USDC (one-time onchain tx)
Buyer->>Seller: 2. Request resource
Seller-->>Buyer: 402 Payment Required
Note left of Buyer: Signs EIP-3009 authorization (offchain, zero gas)
Buyer->>Seller: 3. Retry with signed authorization
Seller->>Gateway: 4. Submit authorization
Seller-->>Buyer: 200 OK + resource
Note right of Gateway: Collects authorizations
Gateway->>Gateway: 5. Batch settle (single onchain tx)
Gateway-->>Seller: Funds credited to balance
```
The buyer deposits USDC from their wallet into a Gateway Wallet contract.
This is a one-time onchain transaction that establishes the buyer's Gateway
balance. Once deposited, the buyer can make gasless payments from this
balance.
The buyer requests a paid resource. The seller responds with `402 Payment
Required`, including payment details. This follows the standard [x402
protocol flow](/gateway/nanopayments/concepts/x402).
The buyer signs an [EIP-3009](/gateway/nanopayments/howtos/eip-3009-signing)
`TransferWithAuthorization` message authorizing Gateway to transfer USDC
from their balance to the seller. This signature is created offchain and
costs zero gas. The buyer retries the request with the signed authorization
attached.
The seller (or a facilitator) submits the signed authorization to Gateway.
Gateway verifies the signature, locks the buyer's funds, and credits the
seller's pending balance. The seller serves the resource immediately,
without waiting for onchain settlement. Neither the buyer nor the seller
pays gas for this step.
Gateway periodically collects pending authorizations, computes net balance
changes across all participants, and submits a single onchain transaction
that applies the changes. After onchain confirmation, pending balances
become available. The seller can then withdraw funds to any supported
blockchain.
## Security model
Gateway's batching system is non-custodial. Gateway never has arbitrary control
over user funds. The security model relies on three components:
### Trusted execution environment (TEE)
A Trusted Execution Environment (TEE) is an isolated hardware enclave that runs
code in a secure, tamper-proof environment. Gateway uses an AWS Nitro Enclave
to:
* Verify every EIP-3009 signature before including it in a batch
* Compute net balance changes across all payments in the batch
* Sign the batch result with the enclave's private key
The enclave's signing key is protected by AWS Key Management Service (KMS) with
attestation-based access policies. Only the audited enclave image can access the
key. Even Circle operators cannot extract it outside the enclave.
### Onchain verification
The Gateway Wallet smart contract verifies the TEE's signature before executing
any batch. If the signature is invalid or comes from an unauthorized signer, the
contract reverts. This ensures that only correctly computed batches are applied
onchain.
### Cryptographic attestations
AWS Nitro Enclaves produce cryptographic attestation documents that prove the
enclave is running a specific, audited code image. These attestations can be
independently verified, providing transparency into the batching process.
## Balance states
Gateway tracks funds through multiple states as they move through the batching
pipeline:
| State | Description |
| ----------- | -------------------------------------------------------------------------------- |
| `available` | Spendable balance. Set after deposit or after receiving a settled batch payment. |
### Example: Alice sends 10 USDC to Bob
| Event | Alice (sender) | Bob (receiver) |
| ----------------------- | ---------------- | --------------- |
| Initial state | `available: 100` | `available: 0` |
| Authorization submitted | `available: 90` | `available: 0` |
| Batch settled onchain | `available: 90` | `available: 10` |
When Alice submits her authorization, Gateway locks her funds internally. Once
the batch settles onchain, Bob's available balance increases.
## Withdrawal
After settlement, sellers (and buyers) can withdraw their Gateway balance to any
supported blockchain. Same-chain withdrawals are instant. Crosschain withdrawals
use Gateway's standard minting infrastructure and are also near instant.
# What is x402?
Source: https://developers.circle.com/gateway/nanopayments/concepts/x402
An overview of the x402 open payment standard and how Nanopayments provides a gasless payment method for x402-protected resources
x402 is an open, neutral standard for internet-native payments built on the HTTP
`402 Payment Required` status code. It defines how a server communicates that
payment is required to access a resource, and how a client can provide proof of
payment. x402 is not a payment system itself—it is a negotiation protocol that
is agnostic to how payments are constructed, verified, or settled.
This document explains the x402 protocol, its core concepts, and how
Nanopayments provides a gasless payment method that works with x402.
## The problem with internet payments
Traditional payment systems were not designed for programmatic, high-frequency
transactions. Credit cards carry high fixed fees, require account creation, and
involve slow settlement. Standard onchain payments require gas for every
transaction, making sub-cent payments uneconomical. Neither approach works well
for AI agents, per-request billing, or machine-to-machine commerce.
x402 addresses this by making payment negotiation a native part of HTTP. A
server declares that payment is required, a client provides a payment payload,
and the exchange happens in a single request-response cycle. The actual payment
method is flexible—any scheme that can produce a verifiable payment payload can
work with x402.
## How x402 works
The x402 protocol uses three HTTP headers to negotiate payment between a client
and a server:
| Header | Direction | Purpose |
| ------------------- | ---------------- | ---------------------------------------------------------------------- |
| `PAYMENT-REQUIRED` | Server to client | Payment requirements (accepted schemes, price, network, destination) |
| `PAYMENT-SIGNATURE` | Client to server | Signed payment payload proving the client has authorized payment |
| `PAYMENT-RESPONSE` | Server to client | Confirmation that the payment was verified, returned with the resource |
The typical flow is:
```mermaid theme={null}
sequenceDiagram
participant Client as Client (Buyer)
participant Server as Server (Seller)
Client->>Server: 1. GET /resource
Server-->>Client: 2. 402 Payment Required + PAYMENT-REQUIRED header
Note left of Client: Signs payment payload
Client->>Server: 3. GET /resource + PAYMENT-SIGNATURE header
Note right of Server: Verifies payment
Server-->>Client: 4. 200 OK + resource + PAYMENT-RESPONSE header
```
1. The client requests a paid resource.
2. The server responds with `402 Payment Required`, including payment details
such as the accepted payment schemes, price, network, and destination
address.
3. The client selects a payment option, constructs and signs a payment payload,
and retries the request with the `PAYMENT-SIGNATURE` header.
4. The server verifies the payment (directly or through a facilitator) and
returns the resource along with a confirmation in the `PAYMENT-RESPONSE`
header.
x402 defines this negotiation flow. How the payment payload is constructed, how
it is verified, and how funds ultimately move are determined by the payment
method and facilitator, not by x402 itself.
## Core concepts
### Buyers and sellers
* **Buyer (client)**: The entity requesting a paid resource. This can be a
human-operated application, an AI agent, or any programmatic HTTP client.
Buyers construct payment payloads using whatever payment method the server
accepts.
* **Seller (server)**: The resource provider that requires payment. Sellers
declare their accepted payment methods in the `402` response, verify incoming
payment payloads, and serve the resource when payment is valid. Any
HTTP-accessible API or service can act as a seller.
### Facilitators
A facilitator is an optional service that handles payment verification and
settlement on behalf of sellers. By using a facilitator, sellers avoid needing
to verify payment payloads or interact with blockchain infrastructure
themselves.
Different facilitators can support different payment methods. A seller connects
to a facilitator and automatically gains access to the payment methods that
facilitator supports.
### Payment schemes
x402 is designed to support multiple payment schemes. A payment scheme defines
how payment payloads are constructed, signed, and verified. The `402` response
from a server lists the schemes it accepts, and the client picks one it can
fulfill.
Nanopayments uses the `exact` scheme with a custom EIP-3009
`TransferWithAuthorization` signature against the `GatewayWalletBatched` domain,
enabling gasless payments from the buyer's Gateway balance.
## How Nanopayments fits in
x402 defines the negotiation—a server says "pay me" and a client responds with a
payment payload. But x402 does not prescribe how those payments are funded,
verified, or settled. That is where Nanopayments comes in.
Nanopayments is a payment method for x402 that uses Circle Gateway's
[batched settlement](/gateway/nanopayments/concepts/batched-settlement)
infrastructure:
* Buyers fund their payments from a Gateway Wallet balance (deposited once
onchain).
* When a server requests payment via `402`, the buyer signs an offchain EIP-3009
authorization (zero gas) and includes it in the `PAYMENT-SIGNATURE` header.
* The server (or its facilitator) submits the authorization to Gateway for
verification and settlement.
* Gateway collects authorizations and settles net positions in bulk onchain,
paying gas once per batch instead of once per payment.
From x402's perspective, Nanopayments is just another payment method. Clients
and servers use the same `402` negotiation flow—the difference is that the
underlying payment is gasless and settled through batching.
## Learn more
* [How batched settlement works](/gateway/nanopayments/concepts/batched-settlement):
the mechanics of Gateway's batching system
* [x402.org](https://www.x402.org/): the official x402 website
* [x402 documentation](https://docs.x402.org/): the full protocol specification
* [x402 GitHub repository](https://github.com/coinbase/x402): the open source
reference implementation
# How-to: Sign EIP-3009 payment authorizations
Source: https://developers.circle.com/gateway/nanopayments/howtos/eip-3009-signing
Manually construct and sign EIP-3009 TransferWithAuthorization messages for Circle Gateway batched payments
## Overview
* Construct and sign EIP-3009 `TransferWithAuthorization` messages that
authorize Circle Gateway to transfer USDC from your Gateway balance.
* Use this when integrating payment signing into a custom workflow, building a
non-JavaScript client, or understanding the signing mechanism that the SDK
handles automatically.
## Prerequisites
Before you begin, ensure you have:
* An EVM wallet with a private key for signing.
* Deposited USDC in a Gateway Wallet contract (see the
[buyer quickstart](/gateway/nanopayments/quickstarts/buyer)).
* Familiarity with [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data
signing.
* Installed the [viem](https://viem.sh/) library (`npm install viem`).
## Steps
### Step 1. Construct the EIP-712 domain
Gateway uses a custom EIP-712 domain named `GatewayWalletBatched`. This is
specific to Gateway's batching feature and is not the standard USDC domain.
```ts sign.ts theme={null}
const domain = {
name: "GatewayWalletBatched",
version: "1",
chainId: 5042002, // Arc Testnet — replace with your target chain's EVM chain ID
verifyingContract: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9", // GatewayWallet on Arc Testnet
};
```
The `verifyingContract` is the GatewayWallet contract address for the blockchain
you are transacting on. Find the address for your target chain in the
[EVM contract addresses](/gateway/references/contract-addresses) reference. You
can also retrieve it programmatically using `getVerifyingContract()` from the
SDK or from the `402` response's `accepts` array (in the
`extra.verifyingContract` field).
The `chainId` must be the standard EVM chain ID for your target network (for
example, `5042002` for Arc Testnet), not the Gateway domain identifier. Using
the wrong chain ID causes the signature to fail silently.
### Step 2. Define the typed data
The `TransferWithAuthorization` type follows the
[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) specification:
```ts sign.ts theme={null}
const types = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
};
```
Populate the message fields.
USDC uses 6 decimal places: `$1.00` = `1000000`, `$0.01` = `10000`, `$0.001` =
`1000`. Always convert dollar amounts to base units before signing.
```ts sign.ts theme={null}
import { randomBytes } from "crypto";
const message = {
from: "0xYOUR_ADDRESS", // Your wallet address (the payer)
to: "0xSELLER_ADDRESS", // The seller's wallet address
value: 10000n, // Amount in USDC base units (0.01 USDC = 10000)
validAfter: 0n, // Signature is valid immediately
validBefore: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 5), // Valid for 5 days
nonce: `0x${randomBytes(32).toString("hex")}`, // Unique random nonce
};
```
The `validBefore` timestamp must be at least 3 days in the future. Gateway
rejects signatures with shorter validity periods to ensure there is enough time
to include them in a settlement batch.
### Step 3. Sign the typed data
Use the `viem` library's `signTypedData` to produce the EIP-712 signature:
```ts sign.ts theme={null}
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const signature = await account.signTypedData({
domain,
types,
primaryType: "TransferWithAuthorization",
message,
});
console.log("Signature:", signature);
```
### Step 4. Assemble and send the payment payload
Encode the payment payload as base64 JSON and attach it to your HTTP request in
the `Payment-Signature` header. The server-side facilitator settles this payment
through the [Settle x402 Payment](/api-reference/gateway/all/settle-x402payment)
API endpoint:
```ts sign.ts theme={null}
const paymentPayload = {
x402Version: 2,
payload: {
authorization: {
from: message.from,
to: message.to,
value: message.value.toString(),
validAfter: message.validAfter.toString(),
validBefore: message.validBefore.toString(),
nonce: message.nonce,
},
signature,
},
resource: "...", // from 402 response
accepted: {}, // the payment option chosen
};
const encoded = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
const response = await fetch("http://localhost:3000/premium-data", {
headers: {
"Payment-Signature": encoded,
},
});
console.log("Status:", response.status);
console.log("Body:", await response.json());
```
### Step 5. Use `BatchEvmScheme` for the x402 protocol (alternative)
If you are integrating with an existing x402 client (such as `@x402/core`), use
the `BatchEvmScheme` class instead of constructing the payload manually. It
handles domain construction, nonce generation, and payload encoding:
```ts sign-x402.ts theme={null}
import { BatchEvmScheme } from "@circle-fin/x402-batching/client";
const batchScheme = new BatchEvmScheme({
address: account.address,
signTypedData: async (params) => account.signTypedData(params),
});
// Get requirements from a 402 response
const res = await fetch(url);
const header = res.headers.get("PAYMENT-REQUIRED");
const { accepts } = JSON.parse(Buffer.from(header, "base64").toString());
const gatewayOption = accepts.find(
(opt) => opt.extra?.name === "GatewayWalletBatched",
);
// Create the payment payload
const payload = await batchScheme.createPaymentPayload(2, gatewayOption);
// Retry with the payload
const finalResponse = await fetch(url, {
headers: {
"Payment-Signature": Buffer.from(
JSON.stringify({ ...payload, accepted: gatewayOption }),
).toString("base64"),
},
});
```
## Troubleshoot `invalid_signature`
Gateway returns `invalid_signature` for any EIP-712 domain or field mismatch
without specifying which field is wrong. If your signature is rejected, check
every item in this list:
* **Domain name** must be exactly `"GatewayWalletBatched"`. Common mistakes
include `"GatewayWallet"`, `"Gateway"`, and `"USDC"`.
* **`verifyingContract`** must be the GatewayWallet contract address, not the
USDC token address or GatewayMinter. See
[EVM contract addresses](/gateway/references/contract-addresses) for the
correct address on each chain.
* **`chainId`** must be the standard EVM chain ID (for example, `5042002` for
Arc Testnet). Do not use the Gateway domain identifier.
* **`nonce`** must be a unique random 32-byte value for every payment. Reusing a
nonce causes the same `invalid_signature` error.
* **`validBefore`** must be at least 3 days in the future. Shorter validity
periods are rejected with `authorization_validity_too_short`.
* **`value`** must be in USDC base units (6 decimals). Passing a dollar amount
instead of base units causes an amount mismatch.
* **`from`** must match the address derived from the private key that signed the
message.
For the full list of error codes, see the
[error reference](/gateway/nanopayments/references/sdk#gateway-api-error-codes).
The EIP-712 domain for payment authorizations (`GatewayWalletBatched`) is
different from the domain used for Gateway withdrawal and crosschain transfer
operations (`GatewayWallet`). If you are building manual signing for both
payments and withdrawals, use the correct domain for each operation. The SDK's
`client.withdraw()` and `client.pay()` methods handle this automatically.
# How-to: Integrate nanopayments into your existing facilitator
Source: https://developers.circle.com/gateway/nanopayments/howtos/facilitator-integration
Add nanopayments gas-free settlement to your x402 facilitator service
## Overview
* Add nanopayments as a settlement provider to your x402 facilitator so that
sellers connected to your facilitator automatically gain access to gas-free,
batched payments.
* Use this guide if you operate an x402 facilitator service and want to offer
Gateway settlement alongside your existing onchain settlement methods.
This guide is for infrastructure providers and payment processors running x402
facilitators.
## Prerequisites
Before you begin, ensure you have:
* An existing x402 facilitator service using `@x402/core`.
* [Node.js](https://nodejs.org/) v18+ installed.
* Familiarity with the [x402 protocol](/gateway/nanopayments/concepts/x402) and
how facilitators verify and settle payments.
## Steps
### Step 1. Install the SDK
Install `@circle-fin/x402-batching` alongside its required peer dependencies:
```shell theme={null}
npm install @circle-fin/x402-batching @x402/core viem
```
### Step 2. Initialize the `BatchFacilitatorClient`
The `BatchFacilitatorClient` handles all communication with Circle Gateway,
including verification via the
[Verify x402 Payment](/api-reference/gateway/all/verify-x402payment) API
endpoint, settlement via the
[Settle x402 Payment](/api-reference/gateway/all/settle-x402payment) API
endpoint, and supported-network discovery via the
[Get Supported x402 Payment Kinds](/api-reference/gateway/all/get-supported-x402payment-kinds)
API endpoint.
```ts theme={null}
import { BatchFacilitatorClient } from "@circle-fin/x402-batching/server";
const gatewayClient = new BatchFacilitatorClient();
```
### Step 3. Route payments by type
Your facilitator acts as a router. Use `isBatchPayment()` to detect Gateway
payments and delegate them to the `BatchFacilitatorClient`. Route all other
payments to your existing onchain logic.
Gateway payments are identified by the `extra` metadata field:
`extra.name === "GatewayWalletBatched"`.
```ts theme={null}
import { isBatchPayment } from "@circle-fin/x402-batching/server";
async function handleVerify(
payload: PaymentPayload,
requirements: PaymentRequirements,
) {
if (isBatchPayment(requirements)) {
return gatewayClient.verify(payload, requirements);
}
return existingOnChainHandler.verify(payload, requirements);
}
async function handleSettle(
payload: PaymentPayload,
requirements: PaymentRequirements,
) {
if (isBatchPayment(requirements)) {
return gatewayClient.settle(payload, requirements);
}
return existingOnChainHandler.settle(payload, requirements);
}
async function handleSupported() {
const gateway = await gatewayClient.getSupported();
const existing = await existingOnChainHandler.getSupported();
return {
kinds: [...existing.kinds, ...gateway.kinds],
extensions: [...existing.extensions, ...gateway.extensions],
signers: { ...existing.signers, ...gateway.signers },
};
}
```
Gateway's `settle()` endpoint is optimized for low latency and guarantees
settlement. Use `settle()` directly rather than calling `verify()` followed by
`settle()` in production flows.
### Step 4. Wire up HTTP endpoints
Expose your routing logic through standard x402 facilitator endpoints:
```ts theme={null}
app.post("/v1/x402/verify", async (req, res) => {
const { paymentPayload, paymentRequirements } = req.body;
const response = await handleVerify(paymentPayload, paymentRequirements);
res.json(response);
});
app.post("/v1/x402/settle", async (req, res) => {
const { paymentPayload, paymentRequirements } = req.body;
const response = await handleSettle(paymentPayload, paymentRequirements);
res.json(response);
});
```
### Step 5. Connect sellers to your facilitator
Once your facilitator supports Gateway, sellers connect to it and automatically
gain access to both standard and gas-free payment options.
#### Using `x402ResourceServer`
Sellers using `x402ResourceServer` connect to your facilitator with
`HTTPFacilitatorClient`:
```ts theme={null}
import { x402ResourceServer } from "@x402/core/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
const server = new x402ResourceServer([
new HTTPFacilitatorClient({ url: "https://your-facilitator-service.com" }),
]);
await server.initialize();
```
#### Using `createGatewayMiddleware`
Sellers using the Gateway middleware can route verification and settlement
through your facilitator by setting `facilitatorUrl`:
```ts theme={null}
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
const gateway = createGatewayMiddleware({
sellerAddress: "0xSELLER_ADDRESS",
facilitatorUrl: "https://your-facilitator-service.com",
});
```
### Alternative: Gas-free-only facilitator
If you are building a new facilitator that only needs to support gas-free
payments (without standard onchain settlement), use `BatchFacilitatorClient`
directly with `x402ResourceServer`:
```shell theme={null}
npm install @x402/core @x402/express @x402/evm
```
```ts theme={null}
import { x402ResourceServer } from "@x402/core/server";
import { BatchFacilitatorClient } from "@circle-fin/x402-batching/server";
const server = new x402ResourceServer([new BatchFacilitatorClient()]);
await server.initialize();
```
#### Using `GatewayEvmScheme` with `x402ResourceServer`
When using `x402ResourceServer` with `BatchFacilitatorClient`, register
`GatewayEvmScheme` to ensure payment requirements include the metadata that
Gateway clients need for EIP-712 signing. `GatewayEvmScheme` extends the
standard `ExactEvmScheme` to:
* Preserve `extra` metadata (`verifyingContract`, `name`, `version`) in payment
requirements
* Set `maxTimeoutSeconds` to 604900 (7 days plus a small buffer) for batched
settlement
* Register USDC money parsers for all Gateway-supported networks
```ts theme={null}
import { x402ResourceServer } from "@x402/express";
import {
BatchFacilitatorClient,
GatewayEvmScheme,
} from "@circle-fin/x402-batching/server";
const circleClient = new BatchFacilitatorClient();
const server = new x402ResourceServer([circleClient]);
server.register("eip155:*", new GatewayEvmScheme());
await server.initialize();
```
The base `ExactEvmScheme` discards the `extra` field from supported kinds when
building payment requirements. Gateway clients require `extra.verifyingContract`
to construct valid EIP-712 signatures. `GatewayEvmScheme` preserves this data.
## See also
* [SDK reference](/gateway/nanopayments/references/sdk) for
`BatchFacilitatorClient` and `GatewayEvmScheme` API details
* [Seller quickstart](/gateway/nanopayments/quickstarts/seller) for setting up a
seller with `createGatewayMiddleware`
* [How batched settlement works](/gateway/nanopayments/concepts/batched-settlement)
for background on the batching lifecycle
# How-to: Add nanopayments to an x402 buyer
Source: https://developers.circle.com/gateway/nanopayments/howtos/x402-buyer
Add gas-free nanopayments to your x402 client alongside standard onchain payments
## Overview
* Add nanopayments to an existing x402 client so it can pay gas-free when a
server supports it, while still falling back to standard onchain payments when
it does not.
* Gas-free payments require a one-time USDC deposit into a Gateway Wallet
contract. After depositing, all subsequent payments are offchain signatures
with zero gas cost.
* Your existing onchain payment flows continue to work unchanged. The client
automatically selects the right payment method based on what the server
offers.
## Prerequisites
Before you begin, ensure you have:
* An existing x402 client using `@x402/core`.
* [Node.js](https://nodejs.org/) v18+ installed.
* An EVM wallet private key for signing.
* Testnet USDC from the [Circle Faucet](https://faucet.circle.com) (for Gateway
deposits).
## Steps
### Step 1. Install the SDK
```shell theme={null}
npm install @circle-fin/x402-batching
```
If you plan to use `CompositeEvmScheme` (recommended for supporting both payment
methods), also install:
```shell theme={null}
npm install @x402/evm
```
### Step 2. Choose your integration approach
Pick the option that best fits your setup.
#### Option A: Support both Gateway and standard payments (recommended)
Use `CompositeEvmScheme` to route automatically based on the server's payment
requirements. When the server offers a Gateway option, the client uses
`BatchEvmScheme` for a gas-free payment. When only standard onchain options are
available, it falls back to `ExactEvmScheme`:
```ts theme={null}
import { ExactEvmScheme } from "@x402/evm/exact/client";
import {
CompositeEvmScheme,
BatchEvmScheme,
} from "@circle-fin/x402-batching/client";
const composite = new CompositeEvmScheme(
new BatchEvmScheme(signer),
new ExactEvmScheme(signer),
);
client.register("eip155:*", composite);
```
No per-request routing code is needed. `CompositeEvmScheme` checks each payment
option's `extra.name` field and delegates to the correct scheme automatically.
#### Option B: Add Gateway support to an existing client
If you already have scheme registrations and want to add Gateway support with
minimal changes while keeping standard onchain payments available:
```ts theme={null}
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { registerBatchScheme } from "@circle-fin/x402-batching/client";
registerBatchScheme(client, {
signer: account,
fallbackScheme: new ExactEvmScheme(account),
});
```
#### Option C: Gateway only
If you only need gas-free payments and don't need standard onchain support, use
`GatewayClient` directly. See the
[buyer quickstart](/gateway/nanopayments/quickstarts/buyer) for a full
walkthrough.
### Step 3. Deposit USDC into Gateway
Before making gas-free payments, deposit USDC from your wallet into the Gateway
Wallet contract. This is a one-time onchain transaction:
```ts theme={null}
import { GatewayClient } from "@circle-fin/x402-batching/client";
const gatewayClient = new GatewayClient({
chain: "arcTestnet",
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
});
const balances = await gatewayClient.getBalances();
if (balances.gateway.available < 1_000_000n) {
await gatewayClient.deposit("1");
}
```
`getBalances()` calls the
[Get Token Balances](/api-reference/gateway/all/get-token-balances) API
endpoint. After the deposit confirms, your Gateway balance is available for
gas-free payments to any server that supports nanopayments. See the discussion
at [Fast deposits](/gateway/references/supported-blockchains#fast-deposits)
about increasing deposit speeds.
### Step 4. Check support before paying
Before attempting a gas-free payment, check whether the target server supports
Gateway. The `supports()` method requests the target URL, checks for a `402`
response, and inspects the `PAYMENT-REQUIRED` header for a compatible Gateway
batching option:
```ts theme={null}
const support = await gatewayClient.supports(
"https://api.example.com/resource",
);
if (!support.supported) {
console.log("Server does not support Gateway -- falling back to onchain");
}
```
If you are using `CompositeEvmScheme`, this check is optional since the scheme
handles fallback automatically.
### Step 5. Understand deposit finality
When you deposit USDC into Gateway, the API waits for block confirmations before
your balance becomes available. Wait times vary by blockchain:
| Blockchain | Deposit time |
| --------------------------------------------------------------------------------------------------------- | ------------ |
| Arc Testnet | \~0.5 sec |
| Avalanche Fuji | \~8 sec |
| HyperEVM Testnet, Sei Atlantic | \~5 sec |
| Polygon PoS Amoy, Sonic Testnet | \~8 sec |
| Arbitrum Sepolia, Base Sepolia, Ethereum Sepolia, Optimism Sepolia, Unichain Sepolia, World Chain Sepolia | \~13-19 min |
To avoid long deposit wait times, see the discussion at
[Fast deposits](/gateway/references/supported-blockchains#fast-deposits) about
third party services that offer fast deposits. Alternatively, use
[Bridge Kit](https://developers.circle.com/circle-mint/docs/bridgekit-quickstart)
to bridge your USDC to a fast-finality blockchain before depositing into Gateway
so your `GatewayClient` can deposit and pay from that chain.
```ts theme={null}
import { BridgeKit } from "@circle-fin/bridge-kit";
const bridge = new BridgeKit();
const transfer = await bridge.transfer({
sourceChain: "base-sepolia",
destinationChain: "arc-testnet",
amount: "10",
token: "USDC",
});
await bridge.waitForCompletion(transfer.id);
const client = new GatewayClient({
chain: "arcTestnet",
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
});
await client.deposit("10");
```
### Step 6. Withdraw funds (optional)
Withdraw USDC from Gateway back to your wallet at any time. Withdrawals are
instant for both same-blockchain and crosschain destinations. The `withdraw()`
method calls the
[Create Transfer Attestation](/api-reference/gateway/all/create-transfer-attestation)
API endpoint:
```ts theme={null}
await gatewayClient.withdraw("5");
await gatewayClient.withdraw("5", { chain: "baseSepolia" });
```
### Step 7. Monitor transfers (optional)
After paying for resources, you can look up individual transfers using the
[Get x402 Transfer by ID](/api-reference/gateway/all/get-x402transfer-by-id) API
endpoint, or search across transfer history using the
[Search x402 Transfers](/api-reference/gateway/all/search-x402transfers) API
endpoint:
```ts theme={null}
const { transfers, pagination } = await gatewayClient.searchTransfers({
status: "received",
pageSize: 20,
});
const transferUUID = transfers[0]?.id;
if (transferUUID) {
try {
const transfer = await gatewayClient.getTransferById(transferUUID);
console.log({
id: transfer.id,
status: transfer.status,
amount: transfer.amount,
});
} catch (error) {
console.error("Could not load transfer", error);
}
}
if (pagination?.pageAfter) {
const nextPage = await gatewayClient.searchTransfers({
status: "received",
pageSize: 20,
pageAfter: pagination.pageAfter,
});
}
```
`searchTransfers` supports filtering by sender, recipient, network, status,
token, and date range. When `network` is omitted, the client defaults to the
client's configured blockchain. See the
[SDK reference](/gateway/nanopayments/references/sdk) for the full list of
parameters.
## See also
* [Use nanopayments with x402](/gateway/nanopayments/howtos/x402-integration)
for a summary of all integration paths
* [x402 seller integration](/gateway/nanopayments/howtos/x402-seller) for the
server-side counterpart
* [SDK reference](/gateway/nanopayments/references/sdk) for
`CompositeEvmScheme`, `BatchEvmScheme`, and `registerBatchScheme` API details
# How-to: Use nanopayments with x402
Source: https://developers.circle.com/gateway/nanopayments/howtos/x402-integration
Add gas-free nanopayments alongside your existing x402 onchain payment flows
Nanopayments integrates with existing x402 setups at every layer of the stack.
It is **additive**: it introduces gas-free payments as a new option alongside
standard onchain x402 payments, without replacing them. Sellers automatically
accept both methods, and buyers choose whichever they have funded.
## Install the SDK
```shell theme={null}
npm install @circle-fin/x402-batching
```
## Integration paths
Choose the guides that match your role in the x402 ecosystem.
Add nanopayments to your x402 client so it can pay gas-free when servers
support it
Add gas-free nanopayments to your existing x402 server alongside your
current payment flows
Route Gateway payments alongside onchain payments in your facilitator
service
### Seller (server-side)
Add `BatchFacilitatorClient` and `GatewayEvmScheme` to your existing server.
Your `402` responses then include both standard and Gateway payment options:
```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();
```
See [Full seller integration guide](/gateway/nanopayments/howtos/x402-seller).
### Buyer (client-side)
Use `CompositeEvmScheme` to handle both Gateway and standard payments
automatically. The client picks the right scheme based on what the server
offers:
```ts theme={null}
import { ExactEvmScheme } from "@x402/evm/exact/client";
import {
CompositeEvmScheme,
BatchEvmScheme,
} from "@circle-fin/x402-batching/client";
const composite = new CompositeEvmScheme(
new BatchEvmScheme(signer),
new ExactEvmScheme(signer),
);
client.register("eip155:*", composite);
```
See [Full buyer integration guide](/gateway/nanopayments/howtos/x402-buyer).
### Facilitator
Use `isBatchPayment()` to route Gateway payments to `BatchFacilitatorClient`
while your existing handler covers standard payments:
```ts theme={null}
import {
BatchFacilitatorClient,
isBatchPayment,
} from "@circle-fin/x402-batching/server";
const gatewayClient = new BatchFacilitatorClient();
async function handleSettle(payload, requirements) {
if (isBatchPayment(requirements)) {
return gatewayClient.settle(payload, requirements);
}
return existingOnChainHandler.settle(payload, requirements);
}
```
See
[Full facilitator integration guide](/gateway/nanopayments/howtos/facilitator-integration).
## How routing works
When both payment methods are available, routing is automatic at every layer:
| Layer | What happens |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server | The `402` response lists both standard and Gateway options in the `accepts` array. |
| Client | `CompositeEvmScheme` checks each option's `extra.name`. If `"GatewayWalletBatched"` is present, it uses `BatchEvmScheme`. Otherwise it uses `ExactEvmScheme`. |
| Facilitator | `isBatchPayment()` checks the same `extra.name` field and routes [verify](/api-reference/gateway/all/verify-x402payment)/[settle](/api-reference/gateway/all/settle-x402payment) calls to either Gateway or the existing onchain handler. |
No manual routing code is needed in sellers or buyers. The SDK handles it based
on the payment requirements.
# How-to: Add nanopayments to an x402 seller
Source: https://developers.circle.com/gateway/nanopayments/howtos/x402-seller
Add gas-free nanopayments to your x402 resource server alongside standard onchain payments
## Overview
* Add nanopayments to an existing `x402ResourceServer` so it offers gas-free
payments alongside your current payment flows.
* Your existing payment setup continues to work unchanged. The server includes
nanopayment options in its `402` responses in addition to the options your
facilitator already supports, and buyers choose whichever method they have
funded.
## Prerequisites
Before you begin, ensure you have:
* An existing x402 seller using `@x402/express` or `@x402/core` with
`x402ResourceServer`.
* [Node.js](https://nodejs.org/) v18+ installed.
## Steps
### Step 1. Install the SDK
```shell theme={null}
npm install @circle-fin/x402-batching @x402/evm
```
### Step 2. Add `BatchFacilitatorClient` to your server
Add `BatchFacilitatorClient` alongside your existing `HTTPFacilitatorClient`.
Each facilitator handles a different payment type -- your existing facilitator
continues to handle standard onchain payments, and `BatchFacilitatorClient`
handles Gateway payments:
```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(),
]);
```
### Step 3. Register `GatewayEvmScheme`
Replace `ExactEvmScheme` with `GatewayEvmScheme`. `GatewayEvmScheme` extends
`ExactEvmScheme`, so standard onchain payments continue to work. It also
preserves the `extra` metadata (such as `verifyingContract`) that Gateway
clients need for EIP-712 signing:
```ts theme={null}
server.register("eip155:*", new GatewayEvmScheme());
await server.initialize();
```
After initialization, the server's `402` responses include Gateway nanopayment
options in the `accepts` array alongside any options your existing facilitator
supports.
If you don't have an existing onchain payment setup or are a new seller, you can
add onchain payment support by connecting to an existing x402 facilitator using
`HTTPFacilitatorClient`. See the [x402 documentation](https://docs.x402.org/)
for a list of available facilitators.
### Step 4. Route through a custom facilitator (optional)
If you run your own x402 facilitator that supports Gateway (see
[facilitator integration](/gateway/nanopayments/howtos/facilitator-integration)),
you can route payments through it instead of connecting to Circle Gateway
directly. Use `facilitatorUrl` with `createGatewayMiddleware`:
```ts theme={null}
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
const gateway = createGatewayMiddleware({
sellerAddress: "0xSELLER_ADDRESS",
facilitatorUrl: "https://your-facilitator.com",
});
```
### Step 5. Check your balance and withdraw
After buyers pay for your resources, funds accumulate in your Gateway balance.
Use `GatewayClient` to check your earnings via the
[Get Token Balances](/api-reference/gateway/all/get-token-balances) API endpoint
and withdraw via the
[Create Transfer Attestation](/api-reference/gateway/all/create-transfer-attestation)
API endpoint:
```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`);
console.log(`Total: ${balances.gateway.formattedTotal} USDC`);
```
Withdraw to your wallet on the same blockchain or to a different one:
```ts theme={null}
await client.withdraw("50");
await client.withdraw("50", { chain: "baseSepolia" });
```
Gateway handles settlement automatically. When you call `settle()` in the
middleware, the payment is submitted for batched processing. Your Gateway
`available` balance increases after the batch settles onchain.
### Step 6. Verify the integration
The server's `402` responses should now include Gateway options alongside
standard options. Check the `accepts` array for entries with:
```ts theme={null}
extra.name === "GatewayWalletBatched";
```
If both standard and Gateway entries are present, the server is correctly
offering both payment methods.
## See also
* [Use nanopayments with x402](/gateway/nanopayments/howtos/x402-integration)
for a summary of all integration paths
* [x402 buyer integration](/gateway/nanopayments/howtos/x402-buyer) for the
client-side counterpart
* [SDK reference](/gateway/nanopayments/references/sdk) for `GatewayEvmScheme`
and `BatchFacilitatorClient` API details
# Quickstart: Pay for resources with nanopayments
Source: https://developers.circle.com/gateway/nanopayments/quickstarts/buyer
Deposit USDC into Gateway and make gasless payments to x402-protected APIs
In this quickstart, you will deposit USDC into a Gateway Wallet, pay for an
x402-protected resource without gas fees, and check your balance. By the end,
you'll have a working client that can make gasless payments to any
x402-compatible API that supports Circle Gateway.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v22.6+](https://nodejs.org/)
* An
[EOA (externally owned account)](/wallets/account-types#externally-owned-accounts-eoa)
wallet private key for signing transactions and payment authorizations.
* Obtained testnet USDC from the [Circle Faucet](https://faucet.circle.com).
* Testnet ETH (or native gas token) for the one-time deposit transaction.
Nanopayments require an EOA wallet. Smart contract account (SCA) wallets are not
supported for nanopayments because the batched settlement path verifies EIP-3009
payment authorizations offchain using `ecrecover`, which is incompatible with
ERC-1271 contract signatures. This limitation is specific to nanopayments;
standard Gateway transfers support
[ERC-1271 signatures](/gateway/references/erc-1271).
## Step 1. Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir nanopayments-buyer
cd nanopayments-buyer
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.pay="node --env-file=.env pay.ts"
# Install runtime dependencies
npm install @circle-fin/x402-batching viem typescript
# Install dev dependencies
npm install --save-dev @types/node
```
### 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_PRIVATE_KEY
```
* `PRIVATE_KEY` is the private key for the EOA you use to deposit USDC and sign
nanopayment authorizations.
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 pay` 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: Initialize the client
Create a new file `pay.ts` and initialize the `GatewayClient` with your chain
and private key:
```ts pay.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}`,
});
```
The `chain` parameter determines which blockchain the client connects to for
deposits and withdrawals. See the
[SDK reference](/gateway/nanopayments/references/sdk) for all supported chain
names.
## Step 3: Deposit USDC into Gateway
Before you can make gasless payments, deposit USDC from your wallet into the
Gateway Wallet contract. This is a one-time onchain transaction:
```ts pay.ts theme={null}
const balances = await client.getBalances();
console.log(`Gateway balance: ${balances.gateway.formattedAvailable} USDC`);
// 1 USDC = 1_000_000 base units (6 decimals)
if (balances.gateway.available < 1_000_000n) {
console.log("Depositing 1 USDC...");
const deposit = await client.deposit("1");
console.log(`Deposit tx: ${deposit.depositTxHash}`);
}
```
`getBalances()` calls the
[Get Token Balances](/api-reference/gateway/all/get-token-balances) API
endpoint. The deposit itself is an onchain transaction and does not use the
Gateway API.
After the deposit confirms, your Gateway balance can be used for gasless
payments to any supported seller. See the discussion at
[Fast deposits](/gateway/references/supported-blockchains#fast-deposits) about
increasing deposit speeds.
## Step 4: Pay for a resource
Add the payment logic to `pay.ts`. Call `client.pay()` with the URL of an
x402-protected resource. The client handles the full payment flow automatically:
1. Sends the initial request to the URL.
2. Receives the `402 Payment Required` response with payment details.
3. Signs an EIP-3009 authorization offchain (zero gas).
4. Retries the request with the `PAYMENT-SIGNATURE` header.
```ts pay.ts theme={null}
const url = "http://localhost:3000/premium-data";
const { data, status } = await client.pay(url);
console.log(`Status: ${status}`);
console.log("Response:", data);
```
Under the hood, `pay()` negotiates the `402` flow and submits the payment
through the [Settle x402 Payment](/api-reference/gateway/all/settle-x402payment)
API endpoint.
Don't have a seller URL to test with? Set up a local test API in two minutes
using the [seller quickstart](/gateway/nanopayments/quickstarts/seller).
## Step 5: Check your balance
Add balance checking after the payment using the
[Get Token Balances](/api-reference/gateway/all/get-token-balances) API
endpoint:
```ts pay.ts theme={null}
const updated = await client.getBalances();
console.log(`Wallet USDC: ${updated.wallet.formatted}`);
console.log(`Gateway available: ${updated.gateway.formattedAvailable}`);
```
## Step 6: Run the script
Run the complete script:
```shell theme={null}
npm run pay
```
You should see the deposit transaction (if needed), the response from the paid
resource, and your updated balance.
## Step 7: Withdraw funds (optional)
You can withdraw USDC from Gateway back to your wallet at any time. Same-chain
withdrawals are instant:
```ts pay.ts theme={null}
const result = await client.withdraw("5");
console.log(`Withdrew ${result.formattedAmount} USDC`);
console.log(`Tx: ${result.mintTxHash}`);
```
To withdraw to a different blockchain:
```ts pay.ts theme={null}
const crossChain = await client.withdraw("5", { chain: "baseSepolia" });
console.log(`Withdrew to ${crossChain.destinationChain}`);
```
Crosschain withdrawals require native gas tokens on the destination blockchain
to cover the minting transaction.
## Check support before paying
Before attempting a payment, you can verify that the target URL supports Gateway
batching. The `supports()` method requests the target URL, checks for a `402`
response, and inspects the `PAYMENT-REQUIRED` header for a compatible Gateway
batching option:
```ts theme={null}
const support = await client.supports(url);
if (!support.supported) {
console.error("This URL does not support Gateway payments");
} else {
const { data } = await client.pay(url);
}
```
This is useful when building clients that interact with APIs where Gateway
support is not guaranteed.
# Quickstart: Accept payments with nanopayments
Source: https://developers.circle.com/gateway/nanopayments/quickstarts/seller
Add gasless USDC payment support to your Express API using Circle Gateway
In this quickstart, you will add Circle Gateway payment middleware to an Express
API so that it accepts gasless USDC payments via the x402 protocol. By the end,
your API will return `402 Payment Required` for unpaid requests and serve
resources when a valid payment signature is provided.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v22.6+](https://nodejs.org/)
* An EVM wallet address where you want to receive USDC payments
## Step 1: Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir nanopayments-seller
cd nanopayments-seller
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="node server.ts"
# Install runtime dependencies
npm install @circle-fin/x402-batching @x402/core @x402/evm viem express typescript
# Install dev dependencies
npm install --save-dev @types/node @types/express
```
### 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
```
## Step 2: Create the server
Create a new file `server.ts` with an Express app and the Gateway middleware:
```ts server.ts theme={null}
import express from "express";
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
import { formatUnits } from "viem";
// Extended Express Request type to include payment information
type PaidRequest = express.Request & {
payment?: {
verified: boolean;
payer: string;
amount: string;
network: string;
transaction?: string;
};
};
const app = express();
const gateway = createGatewayMiddleware({
sellerAddress: "0xYOUR_WALLET_ADDRESS",
facilitatorUrl: "https://gateway-api-testnet.circle.com",
});
```
Replace `0xYOUR_WALLET_ADDRESS` with a valid EVM address where you want to
receive payments. This quickstart uses Arc Testnet, so it points to the testnet
Gateway API. The middleware still accepts payments from
[all supported networks](/gateway/nanopayments/supported-networks).
## Step 3: Protect a route
Use `gateway.require()` to protect any route with a price. When a request
arrives without a valid payment, the middleware returns `402 Payment Required`
with the payment details. When a valid payment signature is attached, the
middleware settles it with Gateway using the
[Settle x402 Payment](/api-reference/gateway/all/settle-x402payment) API
endpoint and calls `next()`:
```ts server.ts theme={null}
app.get("/premium-data", gateway.require("$0.01"), (req: PaidRequest, res) => {
const { payer, amount, network } = req.payment!;
const formattedAmount = formatUnits(BigInt(amount), 6);
console.log(`Paid ${formattedAmount} USDC by ${payer} on ${network}`);
res.json({
secret: "The treasure is hidden under the doormat.",
paid_by: payer,
});
});
app.listen(3000, () => {
console.log("Server listening at http://localhost:3000");
});
```
## Step 4: Test the server
### 4.1. Start the server
```shell theme={null}
npm start
```
### 4.2. Send an unpaid request
In a separate terminal, use `curl` to verify the server returns a `402`
response:
```shell theme={null}
curl -i http://localhost:3000/premium-data
```
You should see a `402 Payment Required` response with a `PAYMENT-REQUIRED`
header. The header contains the payment options.
### 4.3. Pay with a buyer client
Use the [buyer quickstart](/gateway/nanopayments/quickstarts/buyer) client to
make a gasless payment to your server:
```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 { data, status } = await client.pay("http://localhost:3000/premium-data");
console.log(`Status: ${status}`);
console.log("Data:", data);
```
If the payment succeeds, you'll see the JSON response from your protected
endpoint.
## Advanced: Use `BatchFacilitatorClient` directly
If you are not using Express, or need custom logic like dynamic pricing, use the
`BatchFacilitatorClient` directly. The `settle()` method calls the
[Settle x402 Payment](/api-reference/gateway/all/settle-x402payment) API
endpoint:
```ts server.ts expandable theme={null}
import { BatchFacilitatorClient } from "@circle-fin/x402-batching/server";
const facilitator = new BatchFacilitatorClient({
url: "https://gateway-api-testnet.circle.com",
});
const requirements = {
scheme: "exact",
network: "eip155:5042002", // CAIP-2 identifier for Arc Testnet (chain ID 5042002)
asset: "0x...", // USDC contract address on Arc Testnet — see supported chains reference
amount: "10000", // 0.01 USDC
maxTimeoutSeconds: 604900,
payTo: "0xYOUR_ADDRESS", // seller address that receives the payment
extra: {
name: "GatewayWalletBatched",
version: "1",
verifyingContract: "0x...", // Gateway Wallet contract on Arc Testnet — see supported chains reference
},
};
async function handleRequest(paymentSignature?: string) {
if (!paymentSignature) {
const paymentRequired = {
x402Version: 2,
resource: {
url: "/premium-data",
description: "Paid resource",
mimeType: "application/json",
},
accepts: [requirements],
};
return {
status: 402,
headers: {
"PAYMENT-REQUIRED": Buffer.from(
JSON.stringify(paymentRequired),
).toString("base64"),
},
body: {},
};
}
const payload = JSON.parse(
Buffer.from(paymentSignature, "base64").toString("utf8"),
);
const settlement = await facilitator.settle(payload, requirements);
if (!settlement.success) {
return {
status: 402,
body: { error: "Settlement failed" },
};
}
return {
status: 200,
body: { data: "Your paid content" },
};
}
```
Gateway's `settle()` endpoint is optimized for low latency and guarantees
settlement. Use `settle()` directly rather than calling `verify()` followed by
`settle()` in production flows.
## Limit accepted networks (optional)
By default, the middleware accepts payments from any Gateway-supported
blockchain, discovered through the
[Get Supported x402 Payment Kinds](/api-reference/gateway/all/get-supported-x402payment-kinds)
API endpoint. This maximizes your reach since any buyer with a Gateway balance
on one of those accepted networks can pay you.
If you need to restrict payments to specific networks:
```ts theme={null}
const gateway = createGatewayMiddleware({
sellerAddress: "0x...",
networks: ["eip155:5042002"], // Only accept Arc Testnet
});
```
Payment signatures must have at least 7 days plus a small buffer of validity.
The `validBefore` timestamp in the buyer's EIP-3009 authorization must be at
least 7 days in the future, or Gateway will reject it.
# SDK Reference
Source: https://developers.circle.com/gateway/nanopayments/references/sdk
API reference for the @circle-fin/x402-batching SDK
This page documents the public API of the `@circle-fin/x402-batching` package,
which provides buyer and seller integrations for Circle Gateway nanopayments.
```shell theme={null}
npm install @circle-fin/x402-batching
```
## Buyer APIs
### `GatewayClient`
```ts theme={null}
import { GatewayClient } from "@circle-fin/x402-batching/client";
```
The primary client for buyers. Handles deposits, gasless payments, withdrawals,
and balance queries.
#### Constructor
```ts theme={null}
new GatewayClient(config: GatewayClientConfig)
```
| Parameter | Type | Required | Description |
| ------------------- | -------------------- | -------- | ----------------------------------------------------------------------- |
| `config.chain` | `SupportedChainName` | Yes | Blockchain to connect to (for example, `'arcTestnet'`, `'baseSepolia'`) |
| `config.privateKey` | `Hex` | Yes | Private key for signing (`'0x...'`) |
| `config.rpcUrl` | `string` | No | Custom RPC URL (overrides the default for the chain) |
#### Properties
| Property | Type | Description |
| -------------- | -------------- | ---------------------------- |
| `address` | `Address` | The account's wallet address |
| `chainName` | `string` | Human-readable chain name |
| `domain` | `number` | Gateway domain identifier |
| `publicClient` | `PublicClient` | viem public client instance |
| `walletClient` | `WalletClient` | viem wallet client instance |
#### `deposit(amount, options?)`
Deposits USDC from your wallet into the Gateway Wallet contract. This is an
onchain transaction that requires gas.
| Parameter | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------- |
| `amount` | `string` | Yes | Amount in decimal USDC (for example, `'10.5'`) |
| `options.approveAmount` | `string` | No | ERC-20 approval amount (defaults to `amount`) |
Returns `Promise`:
```ts theme={null}
interface DepositResult {
approvalTxHash?: Hex;
depositTxHash: Hex;
amount: bigint;
formattedAmount: string;
}
```
#### `pay(url, options?)`
Pays for an x402-protected resource. Handles the full `402` negotiation flow
automatically: sends the request, receives payment requirements, signs the
authorization, and retries with the payment header.
| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | ---------------------------------------------- |
| `url` | `string` | Yes | URL of the x402-protected resource |
| `options` | `RequestInit` | No | Standard fetch options (method, body, headers) |
Returns `Promise>`:
```ts theme={null}
interface PayResult {
data: T;
amount: bigint;
formattedAmount: string;
transaction: string;
status: number;
}
```
#### `withdraw(amount, options?)`
Withdraws USDC from Gateway to your wallet. Supports same-chain (instant) and
crosschain withdrawals.
| Parameter | Type | Required | Description |
| ------------------- | -------------------- | -------- | ----------------------------------------------- |
| `amount` | `string` | Yes | Amount in decimal USDC |
| `options.chain` | `SupportedChainName` | No | Destination blockchain (defaults to same chain) |
| `options.recipient` | `Address` | No | Recipient address (defaults to your address) |
Returns `Promise`:
```ts theme={null}
interface WithdrawResult {
mintTxHash: Hex;
amount: bigint;
formattedAmount: string;
sourceChain: string;
destinationChain: string;
recipient: Address;
}
```
#### `getBalances(address?)`
Returns both the wallet's USDC balance and the Gateway balance.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | --------------------------------------------------- |
| `address` | `Address` | No | Address to query (defaults to the client's address) |
Returns `Promise`:
```ts theme={null}
interface Balances {
wallet: { balance: bigint; formatted: string };
gateway: GatewayBalance;
}
interface GatewayBalance {
total: bigint;
available: bigint;
withdrawing: bigint;
withdrawable: bigint;
formattedTotal: string;
formattedAvailable: string;
}
```
#### `supports(url)`
Checks whether a URL supports Gateway batching before attempting payment.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ------------ |
| `url` | `string` | Yes | URL to check |
Returns `Promise`:
```ts theme={null}
interface SupportsResult {
supported: boolean;
requirements?: Record;
error?: string;
}
```
#### `getTransferById(id)`
Fetches a single x402 transfer by its UUID.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------- |
| `id` | `string` | Yes | UUID of the transfer to fetch |
Returns `Promise`:
```ts theme={null}
type TransferStatus =
"received" | "batched" | "confirmed" | "completed" | "failed";
interface TransferResponse {
id: string;
status: TransferStatus;
token: "USDC";
sendingNetwork: string;
recipientNetwork: string;
fromAddress: string;
toAddress: string;
amount: string;
createdAt: string;
updatedAt: string;
}
```
#### `searchTransfers(params?)`
Searches x402 transfers with optional filters and paginated results. When
`network` is omitted, the client defaults to its own blockchain.
| Parameter | Type | Required | Description |
| ------------------- | ---------------- | -------- | --------------------------------------------------------------- |
| `params.from` | `Hex` | No | Filter by sender address |
| `params.to` | `Hex` | No | Filter by recipient address |
| `params.network` | `string` | No | CAIP-2 network ID (defaults to the client's blockchain) |
| `params.status` | `TransferStatus` | No | Filter by transfer status |
| `params.token` | `'USDC'` | No | Filter by token type |
| `params.startDate` | `string` | No | ISO-8601 date; include transfers created on or after this date |
| `params.endDate` | `string` | No | ISO-8601 date; include transfers created on or before this date |
| `params.pageSize` | `number` | No | Number of results per page |
| `params.pageAfter` | `string` | No | Cursor for the next page |
| `params.pageBefore` | `string` | No | Cursor for the previous page |
Returns `Promise`:
```ts theme={null}
interface SearchTransfersResponse {
transfers: TransferResponse[];
pagination?: CursorPagination;
}
interface CursorPagination {
self?: string;
first?: string;
prev?: string;
next?: string;
pageAfter?: string;
pageBefore?: string;
}
```
`self`, `first`, `prev`, and `next` are full pagination URLs parsed from the
HTTP `Link` header. `pageAfter` and `pageBefore` are the cursor values extracted
from those URLs, which you can pass back into `searchTransfers()` when
requesting the next or previous page.
Use `pagination.pageAfter` or `pagination.pageBefore` to request adjacent pages.
***
### `BatchEvmScheme`
```ts theme={null}
import { BatchEvmScheme } from "@circle-fin/x402-batching/client";
```
A `SchemeNetworkClient` implementation for Circle Gateway batched payments. Use
this when integrating with an existing `x402Client` instance or building a
custom payment flow.
#### Constructor
```ts theme={null}
new BatchEvmScheme(signer: BatchEvmSigner)
```
| Parameter | Type | Required | Description |
| --------- | ---------------- | -------- | -------------------------------------------------- |
| `signer` | `BatchEvmSigner` | Yes | Object with `address` and `signTypedData` function |
#### `createPaymentPayload(x402Version, paymentRequirements)`
Creates a signed payment payload by constructing and signing an EIP-3009
`TransferWithAuthorization` message.
| Parameter | Type | Required | Description |
| --------------------- | --------------------- | -------- | ------------------------------------ |
| `x402Version` | `number` | Yes | x402 protocol version (use `2`) |
| `paymentRequirements` | `PaymentRequirements` | Yes | Requirements from the `402` response |
Returns `Promise`.
***
### `CompositeEvmScheme`
```ts theme={null}
import { CompositeEvmScheme } from "@circle-fin/x402-batching/client";
```
Routes payment requests between Gateway batched payments and standard onchain
payments. Use this when your client needs to support both payment methods
simultaneously.
#### Constructor
```ts theme={null}
new CompositeEvmScheme(batchScheme: BatchEvmScheme, fallbackScheme: SchemeNetworkClient)
```
| Parameter | Type | Required | Description |
| ---------------- | --------------------- | -------- | ----------------------------------------------------------------- |
| `batchScheme` | `BatchEvmScheme` | Yes | Handles Gateway batched payments |
| `fallbackScheme` | `SchemeNetworkClient` | Yes | Handles standard onchain payments (must use the `'exact'` scheme) |
#### Behavior
* If the payment requirements include `extra.name === "GatewayWalletBatched"`,
delegates to `batchScheme`.
* Otherwise, delegates to `fallbackScheme`.
#### Usage
```ts theme={null}
import { ExactEvmScheme } from "@x402/evm/exact/client";
import {
CompositeEvmScheme,
BatchEvmScheme,
} from "@circle-fin/x402-batching/client";
const composite = new CompositeEvmScheme(
new BatchEvmScheme(signer),
new ExactEvmScheme(signer),
);
client.register("eip155:*", composite);
```
***
### `registerBatchScheme`
```ts theme={null}
import { registerBatchScheme } from "@circle-fin/x402-batching/client";
```
Helper that registers a `BatchEvmScheme` with an `x402Client`. If you need to
support both Gateway and standard onchain `exact` payments, pass a
`fallbackScheme` so one registration handles both paths:
```ts theme={null}
import { ExactEvmScheme } from "@x402/evm/exact/client";
registerBatchScheme(client, {
signer: account,
fallbackScheme: new ExactEvmScheme(account),
});
```
***
## Seller APIs
### `createGatewayMiddleware`
```ts theme={null}
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
```
Creates Express-compatible middleware that handles x402 payment negotiation,
verification, and settlement.
#### Configuration
```ts theme={null}
createGatewayMiddleware(config: GatewayMiddlewareConfig)
```
| Parameter | Type | Required | Description |
| ----------------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `config.sellerAddress` | `string` | Yes | Your wallet address for receiving payments |
| `config.networks` | `string \| string[]` | No | Networks to accept (defaults to all supported) |
| `config.facilitatorUrl` | `string` | No | Custom facilitator URL for verify/settle. Routes through this facilitator instead of Circle Gateway directly. |
| `config.description` | `string` | No | Resource description included in `402` responses |
#### `require(price)`
Returns Express middleware that requires payment for the route.
| Parameter | Type | Description |
| --------- | -------- | ------------------------------------------------- |
| `price` | `string` | Price in USD (for example, `'$0.01'` or `'0.01'`) |
The middleware attaches payment information to `req.payment`:
```ts theme={null}
interface PaymentInfo {
verified: boolean;
payer: string;
amount: string;
network: string;
transaction?: string;
}
```
***
### `BatchFacilitatorClient`
```ts theme={null}
import { BatchFacilitatorClient } from "@circle-fin/x402-batching/server";
```
A `FacilitatorClient` implementation that communicates with Circle Gateway's
x402 endpoints. Use this for custom server frameworks or when you need
fine-grained control over verification and settlement.
#### Constructor
```ts theme={null}
new BatchFacilitatorClient(config?: BatchFacilitatorConfig)
```
#### `verify(payload, requirements)`
Verifies a payment signature through the Gateway API.
| Parameter | Type | Description |
| -------------- | --------------------- | ------------------------------------- |
| `payload` | `PaymentPayload` | The payment payload from the client |
| `requirements` | `PaymentRequirements` | The payment requirements you declared |
Returns `Promise`:
```ts theme={null}
interface VerifyResponse {
isValid: boolean;
invalidReason?: string;
payer?: string;
}
```
#### `settle(payload, requirements)`
Submits a payment for batched settlement through the Gateway API. This is the
recommended method for production flows because it has low latency and
guarantees settlement.
| Parameter | Type | Description |
| -------------- | --------------------- | ------------------------------------- |
| `payload` | `PaymentPayload` | The payment payload from the client |
| `requirements` | `PaymentRequirements` | The payment requirements you declared |
Returns `Promise`:
```ts theme={null}
interface SettleResponse {
success: boolean;
errorReason?: string;
payer?: string;
transaction: string;
}
```
#### `getSupported()`
Fetches the payment kinds (networks and contract addresses) supported by
Gateway.
Returns `Promise`:
```ts theme={null}
interface SupportedResponse {
kinds: Array<{
x402Version: number;
scheme: string;
network: string;
extra?: { verifyingContract?: string };
}>;
}
```
***
### `GatewayEvmScheme`
```ts theme={null}
import { GatewayEvmScheme } from "@circle-fin/x402-batching/server";
```
Server-side EVM scheme that extends `ExactEvmScheme` with Gateway-specific
behavior. Required when using `x402ResourceServer` with
`BatchFacilitatorClient`.
#### Constructor
```ts theme={null}
new GatewayEvmScheme();
```
No parameters required. On construction, it automatically registers USDC money
parsers for all Gateway-supported networks.
#### Key behaviors
* **`enhancePaymentRequirements()`**: Merges `extra` metadata
(`verifyingContract`, `name`, `version`) from supported kinds into payment
requirements. Sets `maxTimeoutSeconds` to `604900` (7 days plus a small
buffer) for batched settlement.
* **USDC money parsers**: Automatically converts dollar amounts to USDC atomic
units (6 decimals) for all supported networks using the USDC addresses from
`CHAIN_CONFIGS`.
The base `ExactEvmScheme` discards the `extra` field from supported kinds when
building payment requirements. Gateway clients require `extra.verifyingContract`
to construct valid EIP-712 signatures. `GatewayEvmScheme` preserves this data.
#### Usage
```ts theme={null}
import { x402ResourceServer } from "@x402/express";
import {
BatchFacilitatorClient,
GatewayEvmScheme,
} from "@circle-fin/x402-batching/server";
const server = new x402ResourceServer([new BatchFacilitatorClient()]);
server.register("eip155:*", new GatewayEvmScheme());
await server.initialize();
```
***
## Utilities
### `supportsBatching`
```ts theme={null}
import { supportsBatching } from "@circle-fin/x402-batching";
```
Checks whether a `PaymentRequirements` object supports Gateway batched
settlement. Returns `true` if the requirements include the
`GatewayWalletBatched` scheme.
### `isBatchPayment`
```ts theme={null}
import { isBatchPayment } from "@circle-fin/x402-batching/server";
```
Server-side alias for `supportsBatching`. Use this in facilitator routing logic
to determine whether a payment should be routed to Gateway.
### `getVerifyingContract`
```ts theme={null}
import { getVerifyingContract } from "@circle-fin/x402-batching";
```
Extracts the Gateway Wallet contract address from a `PaymentRequirements`
object.
***
## Constants
```ts theme={null}
import {
CIRCLE_BATCHING_NAME,
CIRCLE_BATCHING_VERSION,
CIRCLE_BATCHING_SCHEME,
} from "@circle-fin/x402-batching";
```
| Constant | Value | Description |
| ------------------------- | ------------------------ | ----------------------------------------------------- |
| `CIRCLE_BATCHING_NAME` | `'GatewayWalletBatched'` | EIP-712 domain name used for batch payment signatures |
| `CIRCLE_BATCHING_VERSION` | `'1'` | EIP-712 domain version |
| `CIRCLE_BATCHING_SCHEME` | `'exact'` | x402 payment scheme identifier |
***
## Chain configuration
```ts theme={null}
import {
CHAIN_CONFIGS,
GATEWAY_DOMAINS,
} from "@circle-fin/x402-batching/client";
```
* `CHAIN_CONFIGS`: Full configuration (USDC address, Gateway Wallet address, RPC
URL) for all supported blockchains, keyed by `SupportedChainName`.
* `GATEWAY_DOMAINS`: Gateway domain IDs for EIP-712 signing, keyed by chain
name.
Use these constants to retrieve the correct addresses programmatically instead
of hardcoding them:
```ts theme={null}
import {
CHAIN_CONFIGS,
GATEWAY_DOMAINS,
} from "@circle-fin/x402-batching/client";
// Retrieve chain configuration for Arc Testnet
const config = CHAIN_CONFIGS["arcTestnet"];
// config.gatewayWallet → "0x0077777d7EBA4688BDeF3E311b846F25870A19B9"
// config.usdc → "0x3600000000000000000000000000000000000000"
const domainId = GATEWAY_DOMAINS["arcTestnet"]; // → 26
```
For reference, the Gateway Wallet and USDC contract addresses for each chain are
listed in the [EVM contract addresses](/gateway/references/contract-addresses)
and [USDC contract addresses](/stablecoins/usdc-contract-addresses) pages.
***
## Error reference
### Gateway API error codes
These are the `errorReason` (settle) and `invalidReason` (verify) values
returned by the Gateway API:
| Error code | Cause | Recovery |
| ---------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
| `unsupported_scheme` | Scheme is not `exact` | Only the `exact` scheme is supported |
| `unsupported_network` | Network not supported for batching | Use a supported network |
| `unsupported_asset` | Token address not recognized on this blockchain | Use the correct USDC address for the network |
| `invalid_payload` | Malformed payment payload | Check EIP-712 signature fields |
| `address_mismatch` | Authorization `to` address does not match `payTo` | Ensure the payment is addressed to the correct seller |
| `amount_mismatch` | Authorization `value` does not match required `amount` | Ensure the signed amount matches the price |
| `invalid_signature` | EIP-3009 signature verification failed | Re-sign the payment and check the signing key matches `from` |
| `authorization_not_yet_valid` | Authorization `validAfter` is in the future | Wait until the authorization becomes valid |
| `authorization_expired` | Authorization `validBefore` has passed | Create a new payment with a future expiry |
| `authorization_validity_too_short` | Validity window is shorter than 3 days | Set `validBefore` to at least 3 days in the future |
| `self_transfer` | `from` and `to` are the same address | Buyer and seller must be different addresses |
| `insufficient_balance` | Buyer's Gateway balance is too low | Deposit more USDC before paying |
| `nonce_already_used` | Payment nonce was already submitted | Create a new payment (each payment needs a unique nonce) |
| `unsupported_domain` | Gateway domain not configured for this blockchain | Check network configuration |
| `wallet_not_found` | Gateway Wallet contract not found on this blockchain | Verify contract addresses for the network |
| `unexpected_error` | Infrastructure error (HTTP 500) | Retry the request and contact support if the error persists |
### `GatewayClient` errors
All errors are thrown as standard `Error` objects with descriptive messages.
| Error message | Cause | Recovery |
| ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------- |
| `Unsupported chain: {chain}` | Invalid `SupportedChainName` passed to the constructor | Use a valid chain name from the chain configuration table |
| `Request failed with status {N}` | Non-402, non-2xx response from the server | Check server status and URL |
| `Missing PAYMENT-REQUIRED header in 402 response` | Server returned 402 without the required header | Verify the server uses x402 v2 |
| `No payment options in 402 response` | Empty `accepts` array in the 402 response | The server may not have configured payment options |
| `No Gateway batching option available` | Server does not support Gateway batching | Use `supports()` to check first; the server may only accept onchain |
| `Payment failed: {reason}` | Server rejected the payment | Check the Gateway API error code in the message |
| `Insufficient USDC balance` | Wallet USDC balance is less than the deposit amount | Get more USDC from the faucet or transfer from another wallet |
| `Approval transaction failed: {txHash}` | USDC approval transaction reverted onchain | Check gas, wallet balance, and network status |
| `Deposit transaction failed: {txHash}` | Deposit transaction reverted onchain | Check gas and contract state |
| `Insufficient available balance` | Gateway balance is too low for the withdrawal | Deposit more or reduce the amount |
| `Unsupported destination chain: {chain}` | Invalid chain name for crosschain withdrawal | Use a valid `SupportedChainName` |
| `Gateway getTransferById failed ({status}): {details}` | Cannot fetch transfer by ID | Verify the transfer UUID and API connectivity |
| `Gateway searchTransfers failed ({status}): {details}` | Transfer search endpoint returned an error | Validate filters and check API connectivity |
### Middleware errors
| Error message | Cause | Recovery |
| ------------------------------- | ------------------------------------------ | -------------------------------------------------------------------- |
| `Invalid price: {price}` | Price string could not be parsed | Use format `'$0.01'` or `'0.01'` |
| `No payment networks available` | No supported networks found (503 response) | Check Gateway API connectivity |
| `Payment verification failed` | Verify returned `isValid: false` | Check the Gateway API error code in the response body `reason` field |
| `Payment settlement failed` | Settle returned `success: false` | Check the Gateway API error code in the response body `reason` field |
### `BatchFacilitatorClient` errors
| Error message | Cause | Recovery |
| ---------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------- |
| `Circle Gateway verify failed ({status}): {details}` | Verify endpoint returned an unexpected response | Check payload format and API status |
| `Circle Gateway settle failed ({status}): {details}` | Settle endpoint returned an unexpected response | Retry the request and contact support if the error persists |
| `Circle Gateway settle returned empty response ({status})` | Empty body from settle endpoint | Retry the request |
| `Circle Gateway getSupported failed ({status}): {details}` | Cannot fetch supported networks | Check API connectivity and URL |
### `BatchEvmScheme` errors
| Error message | Cause | Recovery |
| -------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------- |
| `BatchEvmScheme: unsupported network format "{network}"` | Network string is not in `eip155:` format | Use CAIP-2 format (for example, `eip155:5042002`) |
| `BatchEvmScheme can only handle Circle batching options` | Payment requirements missing `extra.name="GatewayWalletBatched"` | Use this scheme only for Gateway payments |
| `Circle batching option missing extra.verifyingContract` | Payment requirements missing the Gateway Wallet address | Ensure the server includes `verifyingContract` in `extra` |
# Supported networks
Source: https://developers.circle.com/gateway/nanopayments/supported-networks
Blockchains supported by nanopayments for deposits, gas-free payments, and withdrawals
Nanopayments supports gas-free USDC payments on all EVM blockchains listed in
the [Gateway supported blockchains](/gateway/references/supported-blockchains)
table where the **Nanopayments** column is marked **Yes**.
Use the `SupportedChainName` value from the table when initializing
`GatewayClient` or configuring the SDK:
```typescript theme={null}
const client = new GatewayClient({
chain: "base", // SupportedChainName from the supported blockchains table
privateKey: "0x...",
});
```
When using `GatewayClient`, deposits and payments are scoped to the blockchain
you configure on the client. If you want to pay on Base, initialize the client
for Base and deposit there. To avoid long deposit wait times, use Arc App Kit's
[Bridge Kit](https://docs.arc.io/app-kit/bridge) to bridge your USDC to a
fast-finality blockchain before depositing. The
[Fast deposits](/gateway/references/supported-blockchains#fast-deposits) section
offers third-party alternatives for faster deposits.
## Contract addresses
If you are constructing EIP-712 signatures manually (without the SDK), you need
the contract addresses for your target chain:
* **GatewayWallet address** (used as `verifyingContract` in the EIP-712 domain):
[EVM contract addresses](/gateway/references/contract-addresses)
* **USDC token address** (used as the `asset` in payment requirements):
[USDC contract addresses](/stablecoins/usdc-contract-addresses)
The SDK's `CHAIN_CONFIGS` constant provides these values programmatically. See
the [SDK reference](/gateway/nanopayments/references/sdk#chain-configuration)
for details.
## Testnet faucets
To test nanopayments, you need testnet USDC and native gas tokens:
* [Circle Faucet](https://faucet.circle.com): testnet USDC for supported testnet
blockchains
* For native gas tokens (required for onchain deposit transactions), use the
blockchain-specific faucets listed on each testnet's documentation site
# Quickstart: Gasless Gateway deposits with Eco
Source: https://developers.circle.com/gateway/quickstarts/eco-gasless-deposits
Deposit testnet USDC into Circle Gateway without paying source-chain gas by using Eco's gasless deposit flow.
Use Eco's gasless deposit flow to move 1 testnet USDC from Base Sepolia into
Circle Gateway without holding ETH on the source blockchain. This quickstart
shows a TypeScript script that requests an Eco deposit address, signs an
EIP-3009 authorization, submits the deposit, and checks your Gateway balance on
Polygon PoS Amoy.
Eco is a third-party fast-deposit service. Circle names Eco as an option for
faster Gateway deposits in current docs, but Circle does not operate or audit
Eco's service. Review Eco's docs and test the flow before you use it in
production.
This quickstart uses Base Sepolia as the source blockchain and Polygon PoS Amoy
(Gateway domain ID `7`) as the destination. You sign an EIP-3009
`TransferWithAuthorization` payload locally, and Eco submits the transfer
onchain. You still need USDC in your wallet, but you do not need ETH on Base
Sepolia for the deposit. For other deposit methods and Eco-specific routing
details, see the
[Eco Gateway Deposits guide](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits).
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Obtained a private key for a Base Sepolia EOA
* Funded your wallet with testnet USDC from the
[Circle Faucet](https://faucet.circle.com/) (select **Base Sepolia**)
**Base Sepolia network details**
| Property | Value |
| ------------- | ----------------------------------------------- |
| Chain ID | `84532` |
| USDC contract | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Faucet | [faucet.circle.com](https://faucet.circle.com/) |
You do not need ETH on Base Sepolia for this gasless deposit flow.
## Step 1. Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir eco-gasless-deposit
cd eco-gasless-deposit
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="node --env-file=.env eco-deposit.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=0xYOUR_BASE_SEPOLIA_PRIVATE_KEY
```
* `PRIVATE_KEY` is the private key for the Base Sepolia EOA that holds the
testnet USDC you want to deposit.
* Include the `0x` prefix. MetaMask often copies the private key as bare hex,
but this script expects a hex string with the prefix.
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.
Steps 2 through 6 show individual parts of the flow. Each snippet highlights
one stage and is not independently runnable. Use the full script in [Full
deposit script](#full-deposit-script) to run the quickstart end to end.
## Step 2. Request an Eco deposit address
Request a deterministic Eco deposit address for Gateway on Polygon PoS Amoy.
```typescript theme={null}
const response = await fetch(
"https://deposit-addresses-preproduction.eco.com/api/v1/depositAddresses/gateway/polygon",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: 84532, // Base Sepolia
depositor: yourWalletAddress,
evmDestinationAddress: yourWalletAddress,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to get deposit address: ${await response.text()}`);
}
const { data } = await response.json();
const depositAddress = data.evmDepositAddress;
```
The returned `depositAddress` is a reusable address for this depositor and
destination combination.
## Step 3. Generate EIP-3009 authorization
Create the authorization parameters. EIP-3009 uses a `TransferWithAuthorization`
signed message to let a third party (Eco) submit the transfer on your behalf.
For a deeper explanation of the signing flow, see
[EIP-3009 signing](/gateway/nanopayments/howtos/eip-3009-signing).
```typescript theme={null}
import { randomBytes } from "node:crypto";
const nonce = `0x${randomBytes(32).toString("hex")}`;
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour
const amount = 1000000n; // 1 USDC (6 decimals)
```
Sign the `TransferWithAuthorization` payload:
```typescript theme={null}
const signature = await walletClient.signTypedData({
account,
domain: {
name: "USDC",
version: "2",
chainId: 84532,
verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
},
types: {
TransferWithAuthorization: [
{ 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: "TransferWithAuthorization",
message: {
from: yourWalletAddress,
to: depositAddress,
value: amount,
validAfter,
validBefore,
nonce,
},
});
```
For this Base Sepolia flow, the EIP-712 domain name is `USDC`, not `USD Coin`.
## Step 4. Submit the gasless deposit to Eco
Submit the signed authorization:
```typescript theme={null}
const response = await fetch(
"https://deposit-addresses-preproduction.eco.com/api/v1/gasless/transferWithAuthorization",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: 84532,
from: yourWalletAddress,
to: depositAddress,
value: amount.toString(),
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
nonce,
signature,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to submit transfer: ${await response.text()}`);
}
const { data } = await response.json();
const jobId = data.id;
```
Eco returns a `jobId` you can poll until the deposit completes or fails.
## Step 5. Wait for the deposit to complete
Poll the job status endpoint:
```typescript theme={null}
let status = "PENDING";
let attempts = 0;
const maxAttempts = 30;
while (status === "PENDING" && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(
`https://deposit-addresses-preproduction.eco.com/api/v1/gasless/jobs/${jobId}`,
);
if (!response.ok) {
throw new Error(`Failed to poll status: ${await response.text()}`);
}
const { data } = await response.json();
status = data.status;
attempts++;
console.log(`Status: ${status} (${attempts * 2}s elapsed)`);
}
if (status !== "COMPLETED") {
throw new Error("Deposit failed or timed out");
}
```
## Step 6. Check your Gateway balance
After the job completes, query the Gateway balances endpoint:
```typescript theme={null}
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/balances",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "USDC",
sources: [{ domain: 7, depositor: yourWalletAddress }], // Polygon PoS Amoy
}),
},
);
if (!response.ok) {
throw new Error(`Failed to fetch balance: ${await response.text()}`);
}
const { balances } = await response.json();
const polygonBalance = balances.find((b) => b.domain === 7);
if (!polygonBalance) {
throw new Error("No Polygon PoS Amoy Gateway balance returned");
}
console.log(`Gateway balance: ${polygonBalance.balance} USDC`);
```
The current Gateway balances API returns `balance` as a decimal USDC string for
this flow.
## Full deposit script
This script requests the Eco deposit address, signs the EIP-3009 payload,
submits the gasless deposit, waits for completion, and prints the resulting
Gateway balance. Save it as `eco-deposit.ts`.
```typescript filename="eco-deposit.ts" expandable theme={null}
import { randomBytes } from "node:crypto";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
const USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_CHAIN_ID = 84532;
const POLYGON_AMOY_DOMAIN = 7;
const ECO_BASE_URL = "https://deposit-addresses-preproduction.eco.com/api/v1";
const GATEWAY_API_URL = "https://gateway-api-testnet.circle.com/v1/balances";
const DEPOSIT_AMOUNT = 1000000n; // 1 USDC
async function requestDepositAddress(depositor: `0x${string}`) {
const response = await fetch(
`${ECO_BASE_URL}/depositAddresses/gateway/polygon`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: BASE_SEPOLIA_CHAIN_ID,
depositor,
evmDestinationAddress: depositor,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to get deposit address: ${await response.text()}`);
}
const { data } = await response.json();
return data.evmDepositAddress as `0x${string}`;
}
async function signTransferAuthorization(
walletClient: ReturnType,
account: ReturnType,
from: `0x${string}`,
to: `0x${string}`,
) {
const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600);
const signature = await walletClient.signTypedData({
account,
domain: {
name: "USDC",
version: "2",
chainId: BASE_SEPOLIA_CHAIN_ID,
verifyingContract: USDC_BASE_SEPOLIA as `0x${string}`,
},
types: {
TransferWithAuthorization: [
{ 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: "TransferWithAuthorization",
message: {
from,
to,
value: DEPOSIT_AMOUNT,
validAfter,
validBefore,
nonce,
},
});
return {
nonce,
validAfter,
validBefore,
signature,
};
}
async function submitGaslessDeposit(params: {
from: `0x${string}`;
to: `0x${string}`;
nonce: `0x${string}`;
validAfter: bigint;
validBefore: bigint;
signature: `0x${string}`;
}) {
const response = await fetch(
`${ECO_BASE_URL}/gasless/transferWithAuthorization`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chainId: BASE_SEPOLIA_CHAIN_ID,
from: params.from,
to: params.to,
value: DEPOSIT_AMOUNT.toString(),
validAfter: params.validAfter.toString(),
validBefore: params.validBefore.toString(),
nonce: params.nonce,
signature: params.signature,
}),
},
);
if (!response.ok) {
throw new Error(`Failed to submit transfer: ${await response.text()}`);
}
const { data } = await response.json();
return data.id as string;
}
async function waitForCompletion(jobId: string) {
let status = "PENDING";
let attempts = 0;
const maxAttempts = 30;
while (status === "PENDING" && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const response = await fetch(`${ECO_BASE_URL}/gasless/jobs/${jobId}`);
if (!response.ok) {
throw new Error(`Failed to poll status: ${await response.text()}`);
}
const { data } = await response.json();
status = data.status as string;
attempts++;
console.log(`Status: ${status} (${attempts * 2}s elapsed)`);
}
if (status !== "COMPLETED") {
throw new Error("Deposit failed or timed out");
}
}
async function checkGatewayBalance(depositor: `0x${string}`) {
const response = await fetch(GATEWAY_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: "USDC",
sources: [{ domain: POLYGON_AMOY_DOMAIN, depositor }],
}),
});
if (!response.ok) {
throw new Error(`Failed to fetch balance: ${await response.text()}`);
}
const { balances } = await response.json();
const polygonBalance = balances.find(
(balance: { domain: number; balance: string }) =>
balance.domain === POLYGON_AMOY_DOMAIN,
);
if (!polygonBalance) {
throw new Error("No Polygon PoS Amoy Gateway balance returned");
}
return polygonBalance.balance as string;
}
async function main() {
const rawPrivateKey = process.env.PRIVATE_KEY?.trim();
if (!rawPrivateKey) {
throw new Error("Missing PRIVATE_KEY in .env");
}
const privateKey = (
rawPrivateKey.startsWith("0x") ? rawPrivateKey : `0x${rawPrivateKey}`
) as `0x${string}`;
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
chain: baseSepolia,
transport: http(),
});
console.log(`\nWallet: ${account.address}`);
console.log(`Amount: ${Number(DEPOSIT_AMOUNT) / 1e6} USDC\n`);
console.log("Step 1: Requesting deposit address...");
const depositAddress = await requestDepositAddress(account.address);
console.log(`Deposit address: ${depositAddress}\n`);
console.log("Step 2: Signing authorization...");
const authorization = await signTransferAuthorization(
walletClient,
account,
account.address,
depositAddress,
);
console.log("Signature generated\n");
console.log("Step 3: Submitting to Eco...");
const jobId = await submitGaslessDeposit({
from: account.address,
to: depositAddress,
...authorization,
});
console.log(`Job ID: ${jobId}\n`);
console.log("Step 4: Waiting for completion...");
await waitForCompletion(jobId);
console.log("\nDeposit successful\n");
console.log("Step 5: Checking Gateway balance...");
const gatewayBalance = await checkGatewayBalance(account.address);
console.log(`Gateway balance: ${gatewayBalance} USDC\n`);
}
main().catch((error) => {
if (error instanceof Error) {
console.error("Error:", error.message);
return;
}
console.error("Error:", error);
});
```
Run the script:
```bash theme={null}
npm run start
```
For withdrawal and crosschain transfer capabilities, see the
[Gateway documentation](https://developers.circle.com/gateway). To use your
Gateway balance for Gateway-based USDC flows, explore the
[Arc Nanopayments Demo](https://github.com/circlefin/arc-nanopayments).
# Quickstart: Create and transfer a unified USDC balance on EVM
Source: https://developers.circle.com/gateway/quickstarts/unified-balance-evm
Create and transfer a unified USDC balance on EVM using Circle Gateway.
This guide walks you through the process of creating Unified Crosschain USDC
Balances on
[supported EVM chains](/gateway/references/supported-blockchains#testnet) using
Circle Gateway, and performing transfers from EVM to EVM and from Solana to EVM.
**Use
[Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit)
to simplify this integration.**
This quickstart uses a manual Gateway integration. It is for learning or for
developers who need direct control.
To simplify, use Unified Balance Kit to deposit and spend USDC in just a few
lines of code.
Select a tab below for the Circle Wallets or self-managed wallet path.
## Prerequisites
This quickstart uses Arc Testnet for deposits and the
`Arc Testnet -> Base Sepolia` route for the EVM transfer example. You can adapt
the same code to other supported EVM testnets later in the guide.
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Obtained a
[Circle API Key](/w3s/circle-developer-account#creating-an-api-key-for-developer-services)
and [Entity Secret](/wallets/dev-controlled/register-entity-secret) from the
[Circle Console](/w3s/circle-developer-account).
* [Created developer-controlled wallets](/wallets/dev-controlled/create-your-first-wallet)
for the blockchains you want to test.
* Funded your wallets with testnet tokens:
* Get testnet USDC from the [Circle Faucet](https://faucet.circle.com/).
* Get test native tokens from the
[Console Faucet](https://console.circle.com/faucet).
If you want to try the Solana to EVM transfer, ensure that you've:
* Created a Solana Devnet developer-controlled wallet to act as the source
depositor
* Completed the
[deposit flow](/gateway/quickstarts/unified-balance-solana#step-3-deposit-into-a-unified-crosschain-balance-circle-wallets)
from the
[Solana quickstart](/gateway/quickstarts/unified-balance-solana#circle-wallets)
first
If you want to use the Direct Mint path for Solana to EVM, also create an EVM
developer-controlled wallet on the destination chain to submit the mint
transaction.
### Add testnet funds to your wallet
To interact with Gateway, you need test USDC and native tokens in your wallet on
each chain you deposit from. In the default Arc Testnet deposit flow, the same
USDC also covers transaction fees because Arc uses USDC as the native gas token.
Use the [Circle Faucet](https://faucet.circle.com/) to get test USDC. If you
have a [Circle Developer Console](https://console.circle.com) account, you can
use the [Console Faucet](https://console.circle.com/faucet) to get testnet
native tokens. In addition, the following faucets can also be used to fund your
wallet with testnet native tokens:
**Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `arcTestnet` |
| USDC address | `0x3600000000000000000000000000000000000000` |
| Domain ID | `26` |
**Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `avalancheFuji` |
| USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
| Domain ID | `1` |
**Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `baseSepolia` |
| USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Domain ID | `6` |
**Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sepolia` |
| USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Domain ID | `0` |
**Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `hyperliquidEvmTestnet` |
| USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
| Domain ID | `19` |
**Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `seiTestnet` |
| USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
| Domain ID | `16` |
**Faucet:** [Solana Devnet](https://faucet.solana.com/)
| Property | Value |
| ------------ | ------------------------------------------------------- |
| Chain name | `solanaDevnet` (note that Solana is not EVM-compatible) |
| USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` |
| Domain ID | `5` |
**Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sonicTestnet` |
| USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
| Domain ID | `13` |
**Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `worldchainSepolia` |
| USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
| Domain ID | `14` |
## Step 1. Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir unified-gateway-balance-evm-circle-wallets
cd unified-gateway-balance-evm-circle-wallets
npm init -y
# Set up module type and run scripts
npm pkg set type=module
npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
npm pkg set scripts.balances="node --env-file=.env balances.ts"
npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"
# Install runtime dependencies
npm install @circle-fin/developer-controlled-wallets typescript
# Install dev dependencies
npm install --save-dev @types/node
```
If you want to try the Solana to EVM transfer, add the run script and install
these additional dependencies:
```shell theme={null}
npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
npm install @solana/buffer-layout @solana/web3.js bs58
```
### 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
Create a `.env` file in the project directory:
```text .env theme={null}
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
DEPOSITOR_ADDRESS=YOUR_SOURCE_WALLET_ADDRESS
RECIPIENT_ADDRESS=YOUR_DESTINATION_WALLET_ADDRESS
```
* `CIRCLE_API_KEY` is your Circle API key.
* `CIRCLE_ENTITY_SECRET` is your Circle entity secret.
* `DEPOSITOR_ADDRESS` is the source depositor wallet for the script you are
running.
* `RECIPIENT_ADDRESS` is the destination wallet. It is only required for
transfer scripts.
For `transfer-from-evm.ts`, both `DEPOSITOR_ADDRESS` and `RECIPIENT_ADDRESS` are
EVM addresses.
For `transfer-from-sol.ts`, `DEPOSITOR_ADDRESS` is a Solana address and
`RECIPIENT_ADDRESS` is an EVM address.
For the Circle Wallets Direct Mint path, `RECIPIENT_ADDRESS` must be a
destination-chain Developer-Controlled Wallet address in your Circle account.
The script uses `createContractExecutionTransaction()` to submit
`gatewayMint()` from that wallet, so a regular external EVM address will not
work.
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.
## Step 2. Set up the configuration file
The deposit and transfer scripts share this configuration file.
### 2.1. Create the configuration file
```shell theme={null}
touch config.ts
```
### 2.2. Configure shared Gateway constants
Add the shared Gateway constants used by the deposit and transfer scripts. To
swap chains later, keep `chainConfig` as the shared map and update the
`SOURCE_CHAIN` and `DEST_CHAIN` constants inside each script.
```ts config.ts expandable theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
type WalletChain = "ARC-TESTNET" | "BASE-SEPOLIA";
type EvmChainConfig = {
chainName: string;
usdc: string;
domain: number;
walletChain: WalletChain;
};
export const chainConfig = {
arc: {
chainName: "Arc Testnet",
usdc: "0x3600000000000000000000000000000000000000",
domain: 26,
walletChain: "ARC-TESTNET",
},
base: {
chainName: "Base Sepolia",
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
domain: 6,
walletChain: "BASE-SEPOLIA",
},
} as const satisfies Record;
export type ChainKey = keyof typeof chainConfig;
export const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
export const GATEWAY_WALLET_ADDRESS =
"0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
export const GATEWAY_MINTER_ADDRESS =
"0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
export const API_KEY = process.env.CIRCLE_API_KEY!;
export const ENTITY_SECRET = process.env.CIRCLE_ENTITY_SECRET!;
export const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
if (!API_KEY || !ENTITY_SECRET || !DEPOSITOR_ADDRESS) {
console.error(
"Missing required env vars: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, DEPOSITOR_ADDRESS",
);
process.exit(1);
}
/* Circle Wallets Client */
export const client = initiateDeveloperControlledWalletsClient({
apiKey: API_KEY,
entitySecret: ENTITY_SECRET,
});
export async function waitForTxCompletion(
client: ReturnType,
txId: string,
label: string,
) {
const terminalStates = new Set([
"COMPLETE",
"CONFIRMED",
"FAILED",
"DENIED",
"CANCELLED",
]);
process.stdout.write(`Waiting for ${label} (txId=${txId})\n`);
while (true) {
const { data } = await client.getTransaction({ id: txId });
const state = data?.transaction?.state;
process.stdout.write(".");
if (state && terminalStates.has(state)) {
process.stdout.write("\n");
console.log(`${label} final state: ${state}`);
if (state !== "COMPLETE" && state !== "CONFIRMED") {
throw new Error(
`${label} did not complete successfully (state=${state})`,
);
}
return data.transaction;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
export function parseBalance(
value: string | number | null | undefined,
): bigint {
const str = String(value ?? "0");
const [whole, decimal = ""] = str.split(".");
const decimal6 = (decimal + "000000").slice(0, 6);
return BigInt((whole || "0") + decimal6);
}
export function addressToBytes32(address: string) {
return ("0x" +
address
.toLowerCase()
.replace(/^0x/, "")
.padStart(64, "0")) as `0x${string}`;
}
export function stringifyTypedData(obj: T) {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
```
## Step 3. Deposit into a unified crosschain balance (Circle Wallets)
The deposit script deposits USDC into the Gateway Wallet on Arc Testnet. You can
skip to the [full deposit script](#3-5-full-deposit-script-circle-wallets) if
you prefer.
Do not transfer USDC directly to the Gateway Wallet contract with a standard
ERC-20 transfer. You must call a Gateway deposit method or the USDC will not
be credited to your unified balance.
### 3.1. Create the deposit script
```shell theme={null}
touch deposit.ts
```
### 3.2. Define constants and deposit amount
Set the deposit amount near the top of the script. This example uses Arc Testnet
as the source chain, and you can swap chains later by changing `SOURCE_CHAIN`.
```ts theme={null}
const DEPOSIT_AMOUNT_USDC = "2";
```
### 3.3. Approve USDC spending and submit the deposit
The script first approves the Gateway Wallet contract to spend USDC, then calls
the Gateway `deposit(address,uint256)` method on the configured source chain.
```ts theme={null}
const SOURCE_CHAIN = "arc" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const approveTx = await client.createContractExecutionTransaction({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
contractAddress: sourceConfig.usdc,
abiFunctionSignature: "approve(address,uint256)",
abiParameters: [
GATEWAY_WALLET_ADDRESS,
parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const depositTx = await client.createContractExecutionTransaction({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
contractAddress: GATEWAY_WALLET_ADDRESS,
abiFunctionSignature: "deposit(address,uint256)",
abiParameters: [
sourceConfig.usdc,
parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
```
### 3.4. Wait for Circle Wallet transaction to complete
Circle Wallet contract execution is asynchronous. After each submit step, wait
for transaction completion before proceeding to the next phase.
```ts theme={null}
await waitForTxCompletion(client, approveTxId, "USDC approve");
await waitForTxCompletion(client, depositTxId, "Gateway deposit");
```
### 3.5. Full deposit script (Circle Wallets)
The script approves USDC spending and deposits into the Gateway Wallet on the
configured source chain. Inline comments explain each stage.
```ts deposit.ts expandable theme={null}
import {
chainConfig,
GATEWAY_WALLET_ADDRESS,
DEPOSITOR_ADDRESS,
client,
waitForTxCompletion,
parseBalance,
} from "./config.js";
const DEPOSIT_AMOUNT_USDC = "2";
const SOURCE_CHAIN = "arc" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
async function main() {
console.log(`Using account: ${DEPOSITOR_ADDRESS}`);
console.log(`Depositing on: ${sourceConfig.chainName}`);
// [1] Approve the Gateway Wallet to spend USDC on the source chain.
console.log(
`Approving ${DEPOSIT_AMOUNT_USDC} USDC on ${sourceConfig.chainName}...`,
);
const approveTx = await client.createContractExecutionTransaction({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
contractAddress: sourceConfig.usdc,
abiFunctionSignature: "approve(address,uint256)",
abiParameters: [
GATEWAY_WALLET_ADDRESS,
parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const approveTxId = approveTx.data?.id;
if (!approveTxId) throw new Error("Failed to create approve transaction");
await waitForTxCompletion(client, approveTxId, "USDC approve");
// [2] Call the Gateway deposit function on the source chain.
console.log(`Depositing ${DEPOSIT_AMOUNT_USDC} USDC to Gateway Wallet`);
const depositTx = await client.createContractExecutionTransaction({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
contractAddress: GATEWAY_WALLET_ADDRESS,
abiFunctionSignature: "deposit(address,uint256)",
abiParameters: [
sourceConfig.usdc,
parseBalance(DEPOSIT_AMOUNT_USDC).toString(),
],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const depositTxId = depositTx.data?.id;
if (!depositTxId) throw new Error("Failed to create deposit transaction");
await waitForTxCompletion(client, depositTxId, "Gateway deposit");
console.log(
"\n==| Block confirmation may take up to 19 minutes for some chains |==",
);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 3.6. Run the deposit script
Run the script:
```shell theme={null}
npm run deposit
```
Wait for the
[required number of block confirmations](https://developers.circle.com/gateway/references/supported-blockchains#required-block-confirmations).
Once the deposit transactions are final, the total balance is the sum of all the
USDC from deposit transactions across all supported chains that have reached
finality.
If you later adapt this deposit script to another EVM chain, update
`SOURCE_CHAIN` to another supported entry in `chainConfig`.
### 3.7. Check the balances on the Gateway Wallet
Create a new file called `balances.ts`, and add the following code. This script
retrieves the USDC balances available from your Gateway Wallet for the
`DEPOSITOR_ADDRESS` currently set in `.env`.
```ts balances.ts expandable theme={null}
interface GatewayBalancesResponse {
balances: Array<{
domain: number;
balance: string;
}>;
}
const EVM_DOMAINS = {
base: 6,
arc: 26,
};
const SOLANA_DOMAINS = {
solana: 5,
};
const DOMAINS = { ...EVM_DOMAINS, ...SOLANA_DOMAINS };
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
if (!DEPOSITOR_ADDRESS) {
console.error("Missing required env var: DEPOSITOR_ADDRESS");
process.exit(1);
}
const isEvmAddress = DEPOSITOR_ADDRESS.startsWith("0x");
async function main() {
console.log(`Depositor address: ${DEPOSITOR_ADDRESS}`);
console.log(`Address type: ${isEvmAddress ? "EVM" : "Solana"}\n`);
const activeDomains = isEvmAddress ? EVM_DOMAINS : SOLANA_DOMAINS;
const domainIds = Object.values(activeDomains);
const body = {
token: "USDC",
sources: domainIds.map((domain) => ({
domain,
depositor: DEPOSITOR_ADDRESS,
})),
};
const res = await fetch(
"https://gateway-api-testnet.circle.com/v1/balances",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
const result = (await res.json()) as GatewayBalancesResponse;
let total = 0;
for (const balance of result.balances) {
const chain =
Object.keys(DOMAINS).find(
(k) => DOMAINS[k as keyof typeof DOMAINS] === balance.domain,
) || `Domain ${balance.domain}`;
const amount = parseFloat(balance.balance);
console.log(`${chain}: ${amount.toFixed(6)} USDC`);
total += amount;
}
console.log(`\nTotal: ${total.toFixed(6)} USDC`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
You can run it to check whether finality has been reached for recent
transactions.
```shell theme={null}
npm run balances
```
## Step 4. Transfer USDC from EVM to EVM
This step transfers USDC from your Arc Testnet Gateway balance to Base Sepolia.
Both paths create and submit the burn intent from Arc Testnet first. Direct Mint
then retrieves the Gateway attestation and calls `gatewayMint(...)` on Base
Sepolia from your wallet, while the Forwarding Service lets Circle complete the
destination mint for you.
If your depositor is a smart contract account, sign the burn intent using
ERC-1271 instead. See [Transfer USDC from a smart contract
account](/gateway/howtos/transfer-with-erc-1271).
### 4.1. Create the EVM transfer script (Circle Wallets)
You can skip to the
[full transfer script](#4-7-full-evm-direct-mint-script-circle-wallets) if you
prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the source chain and Base Sepolia as the
destination chain.
This direct-mint flow signs the burn intent on Arc Testnet, requests the Gateway
attestation, and then calls `gatewayMint(...)` on Base Sepolia from your wallet.
```ts theme={null}
type GatewayTransferResponse = {
attestation: string;
signature: string;
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();
```
### 4.3. Add shared chain references
The script uses the shared chain map from `config.ts`.
```ts theme={null}
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destinationConfig = chainConfig[DEST_CHAIN];
```
### 4.4. Create and sign the burn intent
Create an EIP-712 burn intent for the Arc Testnet Gateway balance and sign it
with the source Developer-Controlled Wallet.
```ts theme={null}
const burnIntent = createBurnIntent({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAddress: RECIPIENT_ADDRESS,
});
const typedData = burnIntentTypedData(burnIntent);
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
data: stringifyTypedData(typedData),
});
```
### 4.5. Request the Gateway attestation
Send the signed burn intent to the Gateway API and validate that the response
includes both the attestation and the operator signature needed for minting.
```ts theme={null}
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(requests),
});
```
### 4.6. Mint on Base Sepolia
Once the Gateway API returns the attestation set, call
`gatewayMint(bytes,bytes)` on Base Sepolia and wait for Circle Wallet
transaction completion.
```ts theme={null}
const tx = await client.createContractExecutionTransaction({
walletAddress: RECIPIENT_ADDRESS,
blockchain: destinationConfig.walletChain,
contractAddress: GATEWAY_MINTER_ADDRESS,
abiFunctionSignature: "gatewayMint(bytes,bytes)",
abiParameters: [attestation, operatorSig],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
await waitForTxCompletion(client, txId, "USDC mint");
```
### 4.7. Full EVM direct-mint script (Circle Wallets)
The script builds and signs a burn intent for Arc Testnet, requests the Gateway
attestation, and mints on Base Sepolia. Inline comments explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
chainConfig,
GATEWAY_API_BASE,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
DEPOSITOR_ADDRESS,
client,
waitForTxCompletion,
addressToBytes32,
stringifyTypedData,
} from "./config.js";
type GatewayTransferResponse = {
attestation: string;
signature: string;
};
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!RECIPIENT_ADDRESS) {
console.error("Missing required env var: RECIPIENT_ADDRESS");
process.exit(1);
}
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destinationConfig = chainConfig[DEST_CHAIN];
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
];
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
];
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
];
function createBurnIntent(params: {
depositorAddress: string;
recipientAddress?: string;
}) {
const { depositorAddress, recipientAddress = depositorAddress } = params;
return {
maxBlockHeight: MAX_UINT256_DEC,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: sourceConfig.domain,
destinationDomain: destinationConfig.domain,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceConfig.usdc,
destinationToken: destinationConfig.usdc,
sourceDepositor: depositorAddress,
destinationRecipient: recipientAddress,
sourceSigner: depositorAddress,
destinationCaller: "0x0000000000000000000000000000000000000000",
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
},
};
}
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent",
message: {
...burnIntent,
spec: {
...burnIntent.spec,
sourceContract: addressToBytes32(burnIntent.spec.sourceContract),
destinationContract: addressToBytes32(
burnIntent.spec.destinationContract,
),
sourceToken: addressToBytes32(burnIntent.spec.sourceToken),
destinationToken: addressToBytes32(burnIntent.spec.destinationToken),
sourceDepositor: addressToBytes32(burnIntent.spec.sourceDepositor),
destinationRecipient: addressToBytes32(
burnIntent.spec.destinationRecipient,
),
sourceSigner: addressToBytes32(burnIntent.spec.sourceSigner),
destinationCaller: addressToBytes32(burnIntent.spec.destinationCaller),
},
},
};
}
async function main() {
console.log("Transferring 1 USDC from Arc Testnet to Base Sepolia");
console.log(`Using wallet: ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient: ${RECIPIENT_ADDRESS}`);
// [1] Create and sign the Arc Testnet burn intent.
console.log("\n[1/3] Signing burn intent...");
const burnIntent = createBurnIntent({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAddress: RECIPIENT_ADDRESS,
});
const typedData = burnIntentTypedData(burnIntent);
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) throw new Error("Failed to sign burn intent");
const requests = [{ burnIntent: typedData.message, signature }];
// [2] Submit the burn intent to the Gateway API.
console.log("[2/3] Submitting burn intent to Gateway API...");
const transferResponse = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(requests),
});
if (!transferResponse.ok) {
throw new Error(await transferResponse.text());
}
const { attestation, signature: operatorSig } =
(await transferResponse.json()) as GatewayTransferResponse;
if (!attestation || !operatorSig) {
throw new Error("Invalid Gateway API response");
}
// [3] Mint on Base Sepolia with the returned attestation set.
console.log(`[3/3] Minting on ${destinationConfig.chainName}...`);
const tx = await client.createContractExecutionTransaction({
walletAddress: RECIPIENT_ADDRESS,
blockchain: destinationConfig.walletChain,
contractAddress: GATEWAY_MINTER_ADDRESS,
abiFunctionSignature: "gatewayMint(bytes,bytes)",
abiParameters: [attestation, operatorSig],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const txId = tx.data?.id;
if (!txId) throw new Error("Failed to submit mint transaction");
await waitForTxCompletion(client, txId, "USDC mint");
console.log(
`Transfer complete. 1 USDC minted on ${destinationConfig.chainName}.`,
);
console.log(`Mint transaction ID: ${txId}`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the EVM direct-mint script
Run the script to burn from your Arc Testnet Gateway balance and call
`gatewayMint(...)` on Base Sepolia.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
* `RECIPIENT_ADDRESS` is the destination Base Sepolia wallet
* the source depositor has a Gateway balance on Arc Testnet
* the destination wallet can submit the mint transaction on Base Sepolia
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the minted amount and mint
transaction ID.
### 4.1. Create the EVM transfer script
You can skip to the
[full transfer script](#4-7-full-evm-forwarding-script-circle-wallets) if you
prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the source chain and Base Sepolia as the
destination chain.
This forwarding flow signs the burn intent on Arc Testnet, submits it with
forwarding enabled, and then lets Circle complete the destination mint on Base
Sepolia.
```ts theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Define the burn-intent shape
The forwarding flow first estimates the burn-intent limits for the Arc Testnet
to Base Sepolia route, then uses the same burn-intent shape for signing and
submission.
```ts theme={null}
type TransferSpecBytes32 = {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: `0x${string}`;
destinationContract: `0x${string}`;
sourceToken: `0x${string}`;
destinationToken: `0x${string}`;
sourceDepositor: `0x${string}`;
destinationRecipient: `0x${string}`;
sourceSigner: `0x${string}`;
destinationCaller: `0x${string}`;
value: bigint;
salt: `0x${string}`;
hookData: `0x${string}`;
};
```
### 4.4. Estimate forwarding fees
Before you submit the forwarding transfer, call the Gateway estimate endpoint
with `enableForwarder=true`. The response returns the `maxFee` and
`maxBlockHeight` values you must include in the signed burn intent.
```ts theme={null}
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
```
### 4.5. Sign and submit the forwarding burn intent
Sign the burn intent with the Arc Testnet Developer-Controlled Wallet, then
submit it to the Gateway transfer endpoint with `enableForwarder=true`.
```ts theme={null}
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
data: stringifyTypedData(typedData),
});
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Base Sepolia mint for you. Poll the
transfer status endpoint until the transfer is finalized or fails.
```ts theme={null}
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
```
### 4.7. Full EVM forwarding script (Circle Wallets)
The script estimates forwarding fees, signs the burn intent on Arc Testnet,
submits it to the Forwarding Service, and polls until Circle completes the Base
Sepolia mint. Inline comments explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
chainConfig,
GATEWAY_API_BASE,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
DEPOSITOR_ADDRESS,
client,
addressToBytes32,
stringifyTypedData,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
type TransferSpecBytes32 = {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: `0x${string}`;
destinationContract: `0x${string}`;
sourceToken: `0x${string}`;
destinationToken: `0x${string}`;
sourceDepositor: `0x${string}`;
destinationRecipient: `0x${string}`;
sourceSigner: `0x${string}`;
destinationCaller: `0x${string}`;
value: bigint;
salt: `0x${string}`;
hookData: `0x${string}`;
};
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!RECIPIENT_ADDRESS) {
console.error("Missing required env var: RECIPIENT_ADDRESS");
process.exit(1);
}
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destinationConfig = chainConfig[DEST_CHAIN];
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
];
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
];
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
];
function createTransferSpec(): TransferSpecBytes32 {
return {
version: 1,
sourceDomain: sourceConfig.domain,
destinationDomain: destinationConfig.domain,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(sourceConfig.usdc),
destinationToken: addressToBytes32(destinationConfig.usdc),
sourceDepositor: addressToBytes32(DEPOSITOR_ADDRESS),
destinationRecipient: addressToBytes32(RECIPIENT_ADDRESS),
sourceSigner: addressToBytes32(DEPOSITOR_ADDRESS),
destinationCaller: addressToBytes32(
"0x0000000000000000000000000000000000000000",
),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
};
}
async function main() {
console.log("Forwarding 1 USDC from Arc Testnet to Base Sepolia");
console.log(`Using wallet: ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient: ${RECIPIENT_ADDRESS}`);
const spec = createTransferSpec();
// [1] Estimate forwarding fee inputs for the burn intent.
console.log("\n[1/3] Estimating forwarding fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimated = estimateResult.body[0]?.burnIntent;
if (!estimated) throw new Error("Missing burnIntent estimate");
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(
`Forwarding fee: ${Number(fees.forwardingFee) / 1_000_000} ${fees.token}`,
);
}
console.log(`Estimated maxFee: ${Number(maxFee) / 1_000_000} ${fees.token}`);
// [2] Sign and submit the forwarding burn intent.
console.log("\n[2/3] Signing and submitting burn intent...");
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: { maxBlockHeight, maxFee, spec },
};
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: sourceConfig.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) throw new Error("Failed to sign burn intent");
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = (await response.json()) as TransferResponse;
if (!json.transferId) throw new Error("Missing transferId in response");
const transferId = json.transferId;
console.log(`Transfer ID: ${transferId}`);
// [3] Poll until the Forwarding Service completes the destination mint.
console.log("\n[3/3] Polling for transfer completion...");
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferId}`,
);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollRes.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
if (details.transactionHash) {
console.log(`Mint transaction hash: ${details.transactionHash}`);
}
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
if (Date.now() - pollStart >= POLL_TIMEOUT_MS) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`Transfer complete. 1 USDC forwarded to ${destinationConfig.chainName}.`,
);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the EVM forwarding script
Run the script to burn from your Arc Testnet Gateway balance and let the
Forwarding Service mint on Base Sepolia.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
* `RECIPIENT_ADDRESS` is the destination Base Sepolia wallet
* the source depositor has a Gateway balance on Arc Testnet
* no destination gas is required for this forwarding path
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the transfer ID, status, and
destination mint transaction hash.
## Step 4. Transfer USDC from Solana to EVM
This step transfers USDC from your Solana Devnet Gateway balance to Arc Testnet.
Both paths create and sign the Solana burn intent first. Direct Mint then
requests the Gateway attestation and calls `gatewayMint(...)` on Arc Testnet
from your wallet, while the Forwarding Service lets Circle complete the
destination mint for you.
### 4.1. Create the Solana to EVM transfer script
You can skip to the
[full transfer script](#4-7-full-solana-direct-mint-script-circle-wallets) if
you prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the destination chain.
This direct-mint flow signs the burn intent on Solana Devnet, requests the
Gateway attestation, and then calls `gatewayMint(...)` on Arc Testnet from your
wallet.
```ts theme={null}
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
```
### 4.3. Add helper functions
The helper layer encodes the Solana burn intent into the binary layout expected
by Gateway, then normalizes addresses and payloads for the API request.
```ts theme={null}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return "0x" + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
```
### 4.4. Create and sign burn intent
Build the Solana burn intent, prefix the encoded payload, and sign it with the
source Solana Developer-Controlled Wallet.
For Solana-origin transfers, the burn intent uses Solana-specific binary
encoding and must be signed with the Solana signing-domain prefix. See the
[Solana technical guide](/gateway/references/solana#signing) for the signing
rules and message format details.
```ts theme={null}
const burnIntent = createBurnIntent({
sourceDepositor: DEPOSITOR_ADDRESS,
destinationRecipient: RECIPIENT_ADDRESS,
});
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
```
### 4.5. Request attestation from Gateway API
Submit the signed burn intent to the Gateway API and verify that it returns the
attestation and operator signature required by the destination minter.
```ts theme={null}
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(request),
});
```
### 4.6. Mint on Arc Testnet
Use the destination EVM Developer-Controlled Wallet to call
`gatewayMint(bytes,bytes)` on the Gateway Minter contract, then wait for
completion.
```ts theme={null}
const tx = await client.createContractExecutionTransaction({
walletAddress: RECIPIENT_ADDRESS,
blockchain: destConfig.walletChain,
contractAddress: GATEWAY_MINTER_ADDRESS,
abiFunctionSignature: "gatewayMint(bytes,bytes)",
abiParameters: [attestation, operatorSig],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
await waitForTxCompletion(client, txId, "USDC mint");
```
### 4.7. Full Solana direct-mint script (Circle Wallets)
The script signs a Solana burn intent, requests a Gateway attestation, and mints
on Arc Testnet. Inline comments explain each stage.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { PublicKey } from "@solana/web3.js";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import bs58 from "bs58";
import {
chainConfig,
GATEWAY_API_BASE,
GATEWAY_MINTER_ADDRESS,
client,
waitForTxCompletion,
} from "./config.js";
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const DEST_CHAIN = "arc" as const;
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
// Custom layout for Solana PublicKey values in the burn intent payload.
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers.
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
function createBurnIntent(params: {
sourceDepositor: string;
destinationRecipient: string;
}) {
const { sourceDepositor, destinationRecipient } = params;
const destConfig = chainConfig[DEST_CHAIN];
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: destConfig.domain,
sourceContract: solanaAddressToBytes32(SOLANA_GATEWAY_WALLET),
destinationContract: evmAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(SOLANA_USDC),
destinationToken: evmAddressToBytes32(destConfig.usdc),
sourceDepositor: solanaAddressToBytes32(sourceDepositor),
destinationRecipient: evmAddressToBytes32(destinationRecipient),
sourceSigner: solanaAddressToBytes32(sourceDepositor),
destinationCaller: evmAddressToBytes32(
"0x0000000000000000000000000000000000000000",
),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
// Encode the burn intent into the binary layout expected by Gateway.
function encodeBurnIntent(bi: ReturnType): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
// Convert a Solana address to a 32-byte hex string.
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Convert an EVM address to a 32-byte hex string.
function evmAddressToBytes32(address: string): string {
return "0x" + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
// Convert a 32-byte hex string into a Solana PublicKey.
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
// Serialize typed data while converting bigint values to strings.
function stringifyTypedData(obj: T) {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
async function main() {
const destConfig = chainConfig[DEST_CHAIN];
console.log(`Sender (Solana): ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient (EVM): ${RECIPIENT_ADDRESS}`);
console.log(`Transferring from: Solana Devnet -> ${destConfig.chainName}`);
console.log(
`Creating burn intent from Solana Devnet -> ${destConfig.chainName}...`,
);
// [1] Create and sign the Solana burn intent.
const burnIntent = createBurnIntent({
sourceDepositor: DEPOSITOR_ADDRESS,
destinationRecipient: RECIPIENT_ADDRESS,
});
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
const burnIntentSignature = sigResult.data?.signature;
if (!burnIntentSignature) throw new Error("Failed to sign burn intent");
const formattedSignature = burnIntentSignature.startsWith("0x")
? burnIntentSignature
: `0x${burnIntentSignature}`;
const request = [{ burnIntent, signature: formattedSignature }];
console.log("Signed burn intent.");
// [2] Request the attestation set from Gateway API.
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(request),
});
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = (await response.json()) as {
attestation: string;
signature: string;
};
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const attestation = json?.attestation;
const operatorSig = json?.signature;
if (!attestation || !operatorSig) {
throw new Error("Missing attestation or signature in Gateway API response");
}
// [3] Mint on Arc Testnet with the returned attestation.
console.log(`Minting funds on ${destConfig.chainName}...`);
const tx = await client.createContractExecutionTransaction({
walletAddress: RECIPIENT_ADDRESS,
blockchain: destConfig.walletChain,
contractAddress: GATEWAY_MINTER_ADDRESS,
abiFunctionSignature: "gatewayMint(bytes,bytes)",
abiParameters: [attestation, operatorSig],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
const txId = tx.data?.id;
if (!txId) throw new Error("Failed to submit mint transaction");
console.log("Mint tx submitted:", txId);
await waitForTxCompletion(client, txId, "USDC mint");
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction ID (${destConfig.walletChain}):`, txId);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the Solana to EVM direct-mint script
Run the script to burn from your Solana Devnet Gateway balance and call
`gatewayMint(...)` on Arc Testnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
* `RECIPIENT_ADDRESS` is the destination Arc Testnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* the destination Arc Testnet wallet can submit the mint transaction
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the minted amount and mint
transaction ID.
### 4.1. Create the Solana to EVM transfer script
You can skip to the
[full transfer script](#4-6-full-solana-forwarding-script-circle-wallets) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the destination chain.
This forwarding flow signs the burn intent on Solana Devnet, submits it with
forwarding enabled, and then lets Circle complete the destination mint on Arc
Testnet.
```ts theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Estimate forwarding fees
Before you submit the forwarding transfer, call the Gateway estimate endpoint
with `enableForwarder=true`. The response returns the `maxFee` and
`maxBlockHeight` values you must include in the signed burn intent.
```ts theme={null}
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
```
### 4.4. Sign and submit the forwarding burn intent
Create the Solana burn intent with the estimated values, prefix the encoded
payload, and sign it with the source Solana Developer-Controlled Wallet. Then
submit it to the Gateway transfer endpoint with `enableForwarder=true`.
For Solana-origin transfers, the burn intent uses Solana-specific binary
encoding and must be signed with the Solana signing-domain prefix. See the
[Solana technical guide](/gateway/references/solana#signing) for the signing
rules and message format details.
```ts theme={null}
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent, signature }]),
},
);
```
### 4.5. Poll for forwarding completion
The Forwarding Service completes the Arc Testnet mint for you. Poll the transfer
status endpoint until the transfer is finalized or fails.
```ts theme={null}
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
```
### 4.6. Full Solana forwarding script (Circle Wallets)
The script estimates forwarding fees, signs the burn intent on Solana Devnet,
submits it to the Forwarding Service, and polls until Circle completes the Arc
Testnet mint. Inline comments explain each stage.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { PublicKey } from "@solana/web3.js";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import bs58 from "bs58";
import {
chainConfig,
GATEWAY_API_BASE,
GATEWAY_MINTER_ADDRESS,
client,
stringifyTypedData,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const DEST_CHAIN = "arc" as const;
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
function createTransferSpec(params: {
sourceDepositor: string;
destinationRecipient: string;
}) {
const { sourceDepositor, destinationRecipient } = params;
const destConfig = chainConfig[DEST_CHAIN];
return {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: destConfig.domain,
sourceContract: solanaAddressToBytes32(SOLANA_GATEWAY_WALLET),
destinationContract: evmAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(SOLANA_USDC),
destinationToken: evmAddressToBytes32(destConfig.usdc),
sourceDepositor: solanaAddressToBytes32(sourceDepositor),
destinationRecipient: evmAddressToBytes32(destinationRecipient),
sourceSigner: solanaAddressToBytes32(sourceDepositor),
destinationCaller: evmAddressToBytes32(
"0x0000000000000000000000000000000000000000",
),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
};
}
function encodeBurnIntent(bi: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: ReturnType;
}): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return "0x" + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
async function main() {
const destConfig = chainConfig[DEST_CHAIN];
console.log(
`Forwarding 0.1 USDC from Solana Devnet to ${destConfig.chainName}`,
);
console.log(`Using wallet: ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient: ${RECIPIENT_ADDRESS}`);
const spec = createTransferSpec({
sourceDepositor: DEPOSITOR_ADDRESS,
destinationRecipient: RECIPIENT_ADDRESS,
});
// [1] Estimate forwarding fee inputs for the burn intent.
console.log("\n[1/3] Estimating forwarding fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimated = estimateResult.body[0]?.burnIntent;
if (!estimated) throw new Error("Missing burnIntent estimate");
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(`Forwarding fee: ${fees.forwardingFee} ${fees.token}`);
}
console.log(`Estimated maxFee: ${Number(maxFee) / 1_000_000} ${fees.token}`);
// [2] Create, sign, and submit the forwarding burn intent.
console.log("\n[2/3] Signing and submitting burn intent...");
const burnIntent = { maxBlockHeight, maxFee, spec };
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
const signature = sigResult.data?.signature;
if (!signature) throw new Error("Failed to sign burn intent");
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent, signature }]),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = (await response.json()) as TransferResponse;
if (!json.transferId) throw new Error("Missing transferId in response");
const transferId = json.transferId;
console.log(`Transfer ID: ${transferId}`);
// [3] Poll until the Forwarding Service completes the destination mint.
console.log("\n[3/3] Polling for transfer completion...");
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferId}`,
);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollRes.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
if (details.transactionHash) {
console.log(`Mint transaction hash: ${details.transactionHash}`);
}
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
if (Date.now() - pollStart >= POLL_TIMEOUT_MS) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`Transfer complete. 0.1 USDC forwarded to ${destConfig.chainName}.`,
);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.7. Run the Solana forwarding script
Run the script to burn from your Solana Devnet Gateway balance and let the
Forwarding Service mint on Arc Testnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
* `RECIPIENT_ADDRESS` is the destination Arc Testnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* no destination gas is required for this forwarding path
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the transfer ID, status, and
destination mint transaction hash.
## Prerequisites
This quickstart uses Arc Testnet for deposits and the
`Arc Testnet -> Base Sepolia` route for the EVM transfer example. You can adapt
the same code to other supported EVM testnets later in the guide.
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 the
[supported Testnets](/gateway/references/supported-blockchains#testnet) of
your choice to your wallet
If you want to try the Solana to EVM transfer, ensure that you've:
* Prepared a Solana Devnet wallet and exported its keypair as a JSON array
* Completed
[Step 3. Deposit into a unified crosschain balance](/gateway/quickstarts/unified-balance-solana#step-3-deposit-into-a-unified-crosschain-balance-self-managed)
from the
[Solana quickstart](/gateway/quickstarts/unified-balance-solana#self-managed)
### Add testnet funds to your wallet
To interact with Gateway, you need test USDC and native tokens in your wallet on
each chain you deposit from. In the default Arc Testnet deposit flow, the same
USDC also covers transaction fees because Arc uses USDC as the native gas token.
For direct-mint transfers, you also need the destination chain's gas token to
call the Gateway Minter contract.
Use the [Circle Faucet](https://faucet.circle.com/) to get testnet USDC. If you
have a [Circle Developer Console](https://console.circle.com) account, you can
use the [Console Faucet](https://console.circle.com/faucet) to get testnet
native tokens. In addition, the following faucets can also be used to fund your
wallet with testnet native tokens:
**Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `arcTestnet` |
| USDC address | `0x3600000000000000000000000000000000000000` |
| Domain ID | `26` |
**Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `avalancheFuji` |
| USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
| Domain ID | `1` |
**Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `baseSepolia` |
| USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Domain ID | `6` |
**Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sepolia` |
| USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Domain ID | `0` |
**Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `hyperliquidEvmTestnet` |
| USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
| Domain ID | `19` |
**Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `seiTestnet` |
| USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
| Domain ID | `16` |
**Faucet:** [Solana Devnet](https://faucet.solana.com/)
| Property | Value |
| ------------ | ------------------------------------------------------- |
| Chain name | `solanaDevnet` (note that Solana is not EVM-compatible) |
| USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` |
| Domain ID | `5` |
**Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sonicTestnet` |
| USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
| Domain ID | `13` |
**Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `worldchainSepolia` |
| USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
| Domain ID | `14` |
## Step 1. Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir unified-gateway-balance-evm
cd unified-gateway-balance-evm
npm init -y
# Set up module type and run scripts
npm pkg set type=module
npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
npm pkg set scripts.balances="node --env-file=.env balances.ts"
npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"
# Install runtime dependencies
npm install viem typescript
# Install dev dependencies
npm install --save-dev @types/node
```
If you want to try the Solana to EVM transfer, add the run script and install
these additional dependencies:
```shell theme={null}
npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
npm install @solana/buffer-layout @solana/web3.js bs58
```
### 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}
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
```
If you want to try the Solana to EVM transfer, also add:
```text theme={null}
SOLANA_PRIVATE_KEYPAIR=YOUR_SOLANA_KEYPAIR_ARRAY
```
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use for the EVM
side of the transfer.
* `SOLANA_PRIVATE_KEYPAIR` is the Solana wallet keypair as a JSON array for the
Solana side of the transfer.
If your wallet exports a private key hash instead, you can use `bs58` to convert it:
```ts TypeScript theme={null}
const bytes = bs58.decode("YOUR_PRIVATE_KEY_HASH");
console.log(JSON.stringify(Array.from(bytes)));
```
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.
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. Set up the configuration file
This section covers the shared configuration file used by both the deposit and
transfer scripts.
### 2.1. Create the configuration file
```shell theme={null}
touch config.ts
```
### 2.2. Configure shared Gateway constants
Add only the shared values used across the deposit and transfer scripts. This
keeps `config.ts` focused on shared chain data without turning it into a generic
chain-selector layer. To swap chains later, leave `chainConfig` as the shared
map and change the `SOURCE_CHAIN` and `DEST_CHAIN` constants inside each script.
```ts config.ts expandable theme={null}
import { type Address } from "viem";
import { baseSepolia, arcTestnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
/* Account Setup */
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error("EVM_PRIVATE_KEY not set in environment");
}
export const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
/* Gateway Contract Addresses */
export const GATEWAY_WALLET_ADDRESS: Address =
"0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
export const GATEWAY_MINTER_ADDRESS: Address =
"0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
export const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
export const chainConfig = {
arc: {
chain: arcTestnet,
usdcAddress: "0x3600000000000000000000000000000000000000" as Address,
domainId: 26,
},
base: {
chain: baseSepolia,
usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address,
domainId: 6,
},
} as const;
export type ChainKey = keyof typeof chainConfig;
export function stringifyBigInts(obj: T) {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
```
## Step 3. Deposit into a unified crosschain balance (self-managed)
This section explains the deposit script that deposits USDC into the Gateway
Wallet contract on Arc Testnet. You can skip to the
[full deposit script](#3-5-full-deposit-script-self-managed) if you prefer.
### 3.1. Create the script file
```shell theme={null}
touch deposit.ts
```
### 3.2. Define constants and ABI
```ts deposit.ts theme={null}
const DEPOSIT_AMOUNT = 2_000000n; // 2 USDC (6 decimals)
// Gateway Wallet ABI (minimal - only deposit function)
const gatewayWalletAbi = [
{
type: "function",
name: "deposit",
inputs: [
{ name: "token", type: "address" },
{ name: "value", type: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
```
### 3.3. Setup clients and check balances
Set up the client and contracts for the chain, then verify sufficient USDC
balance before depositing.
```ts deposit.ts theme={null}
const SOURCE_CHAIN = "arc" as const;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const chainName = sourceConfig.chain.name;
const client = createPublicClient({
chain: sourceConfig.chain,
transport: http(),
});
// Get contract instances
const usdcContract = getContract({
address: sourceConfig.usdcAddress,
abi: erc20Abi,
client,
});
const gatewayWallet = getContract({
address: GATEWAY_WALLET_ADDRESS,
abi: gatewayWalletAbi,
client,
});
// Check USDC balance
const balance = await usdcContract.read.balanceOf([account.address]);
console.log(`Current balance: ${formatUnits(balance, 6)} USDC`);
if (balance < DEPOSIT_AMOUNT) {
throw new Error(
"Insufficient USDC balance. Please top up at https://faucet.circle.com",
);
}
```
### 3.4. Approve and deposit USDC
The main logic performs two key actions:
* Approve USDC transfers: It calls the `approve` method on the USDC contract to
allow the Gateway Wallet contract to transfer USDC from your wallet.
* Deposit USDC into Gateway: After receiving the approval transaction hash, it
calls the `deposit` method on the Gateway Wallet contract.
```ts deposit.ts theme={null}
// [1] Approve Gateway Wallet to spend USDC
console.log(
`Approving ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC on ${chainName}...`,
);
const approvalTx = await usdcContract.write.approve(
[GATEWAY_WALLET_ADDRESS, DEPOSIT_AMOUNT],
{ account },
);
await client.waitForTransactionReceipt({ hash: approvalTx });
console.log(`Approved on ${chainName}: ${approvalTx}`);
// [2] Deposit USDC into Gateway Wallet
console.log(
`Depositing ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC to Gateway Wallet`,
);
const depositTx = await gatewayWallet.write.deposit(
[sourceConfig.usdcAddress, DEPOSIT_AMOUNT],
{ account },
);
await client.waitForTransactionReceipt({ hash: depositTx });
console.log(`Done on ${chainName}. Deposit tx: ${depositTx}`);
```
### 3.5. Full deposit script (Self-managed)
The complete deposit script checks the source-chain USDC balance, then deposits
funds into the Gateway Wallet contract. The script includes inline comments to
explain each stage.
```ts deposit.ts expandable theme={null}
import {
createPublicClient,
getContract,
http,
erc20Abi,
formatUnits,
} from "viem";
import { account, chainConfig, GATEWAY_WALLET_ADDRESS } from "./config.js";
const DEPOSIT_AMOUNT = 2_000000n; // 2 USDC (6 decimals)
const SOURCE_CHAIN = "arc" as const;
// Gateway Wallet ABI (minimal - only deposit function)
const gatewayWalletAbi = [
{
type: "function",
name: "deposit",
inputs: [
{ name: "token", type: "address" },
{ name: "value", type: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
async function main() {
const sourceConfig = chainConfig[SOURCE_CHAIN];
console.log(`Using account: ${account.address}\n`);
console.log(`Depositing on: ${sourceConfig.chain.name}\n`);
// Create the client for the source chain.
const client = createPublicClient({
chain: sourceConfig.chain,
transport: http(),
});
// Get the USDC and Gateway Wallet contracts.
const usdcContract = getContract({
address: sourceConfig.usdcAddress,
abi: erc20Abi,
client,
});
const gatewayWallet = getContract({
address: GATEWAY_WALLET_ADDRESS,
abi: gatewayWalletAbi,
client,
});
// [1] Check the current source-chain USDC balance.
const balance = await usdcContract.read.balanceOf([account.address]);
console.log(`Current balance: ${formatUnits(balance, 6)} USDC`);
if (balance < DEPOSIT_AMOUNT) {
throw new Error(
"Insufficient USDC balance. Please top up at https://faucet.circle.com",
);
}
// [2] Approve the Gateway Wallet to spend USDC.
console.log(
`Approving ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC on ${sourceConfig.chain.name}...`,
);
const approvalTx = await usdcContract.write.approve(
[GATEWAY_WALLET_ADDRESS, DEPOSIT_AMOUNT],
{ account },
);
await client.waitForTransactionReceipt({ hash: approvalTx });
console.log(`Approved on ${sourceConfig.chain.name}: ${approvalTx}`);
// [3] Deposit USDC into the source-chain Gateway Wallet.
console.log(
`Depositing ${formatUnits(DEPOSIT_AMOUNT, 6)} USDC to Gateway Wallet`,
);
const depositTx = await gatewayWallet.write.deposit(
[sourceConfig.usdcAddress, DEPOSIT_AMOUNT],
{ account },
);
await client.waitForTransactionReceipt({ hash: depositTx });
console.log(`Done on ${sourceConfig.chain.name}. Deposit tx: ${depositTx}`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 3.6. Run the script to create a crosschain balance
Run the deposit script to make the source-chain deposit.
```shell theme={null}
npm run deposit
```
Wait for the
[required number of block confirmations](https://developers.circle.com/gateway/references/supported-blockchains#required-block-confirmations).
Once the deposit transactions are final, the total balance is the sum of all the
USDC from deposit transactions across all supported chains that have reached
finality. Note that for certain chains, finality may take up to 20 minutes to be
reached.
To adapt this deposit script later, update `SOURCE_CHAIN` to another supported
entry in `chainConfig`.
### 3.7. Check the balances on the Gateway Wallet
Create a new file called `balances.ts`, and add the following code. This script
retrieves the USDC balances available from your Gateway Wallet across the
supported testnet routes in `chainConfig`.
```ts balances.ts expandable theme={null}
import { privateKeyToAccount } from "viem/accounts";
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error("Missing EVM_PRIVATE_KEY in environment");
}
const DOMAINS = {
baseSepolia: 6,
arcTestnet: 26,
};
async function main() {
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const depositor = account.address;
console.log(`Depositor address: ${depositor}\n`);
const body = {
token: "USDC",
sources: Object.entries(DOMAINS).map(([_, domain]) => ({
domain,
depositor,
})),
};
const res = await fetch(
"https://gateway-api-testnet.circle.com/v1/balances",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
const result = await res.json();
let total = 0;
for (const balance of result.balances) {
const chain =
Object.keys(DOMAINS).find(
(key) => DOMAINS[key as keyof typeof DOMAINS] === balance.domain,
) || `Domain ${balance.domain}`;
const amount = parseFloat(balance.balance);
console.log(`${chain}: ${amount.toFixed(6)} USDC`);
total += amount;
}
console.log(`\nTotal: ${total.toFixed(6)} USDC`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
You can run it to check whether finality has been reached for recent
transactions.
```shell theme={null}
npm run balances
```
## Step 4. Transfer USDC from EVM to EVM
This step transfers USDC from your Arc Testnet Gateway balance to Base Sepolia.
Both paths create and submit the burn intent from Arc Testnet first. Direct Mint
then retrieves the Gateway attestation and calls `gatewayMint(...)` on Base
Sepolia from your wallet, while the Forwarding Service lets Circle complete the
destination mint for you.
### 4.1. Create the script file (Self-managed)
You can skip to the
[full transfer script](#4-7-full-evm-transfer-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the source chain and Base Sepolia as the
destination chain.
```ts transfer-from-evm.ts theme={null}
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
// EIP-712 Domain and Types for Gateway burn intents
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
const gatewayMinterAbi = [
{
type: "function",
name: "gatewayMint",
inputs: [
{ name: "attestationPayload", type: "bytes" },
{ name: "signature", type: "bytes" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
```
### 4.3. Add helper functions
```ts transfer-from-evm.ts theme={null}
// Create a burn intent for crosschain transfer
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
function createBurnIntent(params: {
depositorAddress: string;
recipientAddress?: string;
}) {
const { depositorAddress, recipientAddress = depositorAddress } = params;
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destConfig = chainConfig[DEST_CHAIN];
return {
maxBlockHeight: maxUint256,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: sourceConfig.domainId,
destinationDomain: destConfig.domainId,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceConfig.usdcAddress,
destinationToken: destConfig.usdcAddress,
sourceDepositor: depositorAddress,
destinationRecipient: recipientAddress,
sourceSigner: depositorAddress,
destinationCaller: zeroAddress,
value: TRANSFER_VALUE,
salt: ("0x" + randomBytes(32).toString("hex")) as Hex,
hookData: "0x" as Hex,
},
};
}
// Create EIP-712 typed data for signing
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: {
...burnIntent,
spec: {
...burnIntent.spec,
sourceContract: addressToBytes32(burnIntent.spec.sourceContract),
destinationContract: addressToBytes32(
burnIntent.spec.destinationContract,
),
sourceToken: addressToBytes32(burnIntent.spec.sourceToken),
destinationToken: addressToBytes32(burnIntent.spec.destinationToken),
sourceDepositor: addressToBytes32(burnIntent.spec.sourceDepositor),
destinationRecipient: addressToBytes32(
burnIntent.spec.destinationRecipient,
),
sourceSigner: addressToBytes32(burnIntent.spec.sourceSigner),
destinationCaller: addressToBytes32(burnIntent.spec.destinationCaller),
},
},
};
}
// Convert address to bytes32
function addressToBytes32(address: string): Hex {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
```
### 4.4. Create and sign burn intents
```ts transfer-from-evm.ts theme={null}
const intent = createBurnIntent({
depositorAddress: account.address,
});
const typedData = burnIntentTypedData(intent);
const signature = await account.signTypedData(typedData);
const requests = [{ burnIntent: typedData.message, signature }];
console.log("Signed burn intent.");
```
### 4.5. Request attestation from Gateway API
```ts transfer-from-evm.ts theme={null}
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts(requests),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = await response.json();
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const attestation = json?.attestation;
const operatorSig = json?.signature;
if (!attestation || !operatorSig) {
throw new Error("Missing attestation or signature in response");
}
```
### 4.6. Mint on Base Sepolia
```ts transfer-from-evm.ts theme={null}
const destConfig = chainConfig[DEST_CHAIN];
const destClient = createPublicClient({
chain: destConfig.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: destConfig.chain,
transport: http(),
});
const destinationGatewayMinterContract = getContract({
address: GATEWAY_MINTER_ADDRESS,
abi: gatewayMinterAbi,
client: { public: destClient, wallet: walletClient },
});
console.log(`Minting funds on ${destConfig.chain.name}...`);
const mintTx = await destinationGatewayMinterContract.write.gatewayMint(
[attestation, operatorSig],
{ account },
);
await destClient.waitForTransactionReceipt({ hash: mintTx });
const totalMinted = BigInt(requests.length) * TRANSFER_VALUE;
console.log(`Minted ${formatUnits(totalMinted, 6)} USDC`);
console.log(`Mint transaction hash (${destConfig.chain.name}):`, mintTx);
```
### 4.7. Full EVM transfer script (Self-managed)
The complete transfer script creates and signs a burn intent for Arc Testnet,
submits it to the Gateway API for attestation, and mints USDC on Base Sepolia.
The script includes inline comments to explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import {
createPublicClient,
createWalletClient,
getContract,
http,
pad,
zeroAddress,
maxUint256,
formatUnits,
type Hex,
} from "viem";
import { randomBytes } from "node:crypto";
import {
account,
chainConfig,
GATEWAY_API_BASE,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
stringifyBigInts,
} from "./config.js";
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
// EIP-712 Domain and Types for Gateway burn intents
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
const gatewayMinterAbi = [
{
type: "function",
name: "gatewayMint",
inputs: [
{ name: "attestationPayload", type: "bytes" },
{ name: "signature", type: "bytes" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
async function main() {
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destConfig = chainConfig[DEST_CHAIN];
console.log(`Using account: ${account.address}`);
console.log(
`Transferring from: ${sourceConfig.chain.name} -> ${destConfig.chain.name}`,
);
// [1] Create and sign the Arc Testnet burn intent.
console.log(
`Creating burn intent from ${sourceConfig.chain.name} -> ${destConfig.chain.name}...`,
);
const burnIntent = {
maxBlockHeight: maxUint256,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: sourceConfig.domainId,
destinationDomain: destConfig.domainId,
sourceContract: pad(GATEWAY_WALLET_ADDRESS.toLowerCase() as Hex, {
size: 32,
}),
destinationContract: pad(GATEWAY_MINTER_ADDRESS.toLowerCase() as Hex, {
size: 32,
}),
sourceToken: pad(sourceConfig.usdcAddress.toLowerCase() as Hex, {
size: 32,
}),
destinationToken: pad(destConfig.usdcAddress.toLowerCase() as Hex, {
size: 32,
}),
sourceDepositor: pad(account.address.toLowerCase() as Hex, { size: 32 }),
destinationRecipient: pad(account.address.toLowerCase() as Hex, {
size: 32,
}),
sourceSigner: pad(account.address.toLowerCase() as Hex, { size: 32 }),
destinationCaller: pad(zeroAddress as Hex, { size: 32 }),
value: TRANSFER_VALUE,
salt: ("0x" + randomBytes(32).toString("hex")) as Hex,
hookData: "0x" as Hex,
},
};
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: burnIntent,
};
const signature = await account.signTypedData(typedData);
const requests = [{ burnIntent: typedData.message, signature }];
console.log("Signed burn intent.");
// [2] Request attestation from Gateway API
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts(requests),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = await response.json();
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const attestation = json?.attestation;
const operatorSig = json?.signature;
if (!attestation || !operatorSig) {
throw new Error("Missing attestation or signature in response");
}
// [3] Mint on destination chain
const destClient = createPublicClient({
chain: destConfig.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: destConfig.chain,
transport: http(),
});
const destinationGatewayMinterContract = getContract({
address: GATEWAY_MINTER_ADDRESS,
abi: gatewayMinterAbi,
client: { public: destClient, wallet: walletClient },
});
console.log(`Minting funds on ${destConfig.chain.name}...`);
const mintTx = await destinationGatewayMinterContract.write.gatewayMint(
[attestation, operatorSig],
{ account },
);
await destClient.waitForTransactionReceipt({ hash: mintTx });
const totalMinted = TRANSFER_VALUE;
console.log(`Minted ${formatUnits(totalMinted, 6)} USDC`);
console.log(`Mint transaction hash (${destConfig.chain.name}):`, mintTx);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the EVM direct-mint script
Run the script to burn from your Arc Testnet Gateway balance and call
`gatewayMint(...)` on Base Sepolia.
Confirm these values before running:
* `EVM_PRIVATE_KEY` controls the source Arc Testnet account
* the source account has a Gateway balance on Arc Testnet
* the destination account has enough Base Sepolia ETH to submit the mint
transaction
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the minted amount and mint
transaction hash.
### 4.1. Create the script file
You can skip to the
[full transfer script](#4-7-full-evm-forwarding-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the source chain and Base Sepolia as the
destination chain.
This forwarding flow signs the burn intent on Arc Testnet, submits it with
forwarding enabled, and then lets Circle complete the destination mint on Base
Sepolia.
```ts transfer-from-evm.ts theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
// EIP-712 Domain and Types for Gateway burn intents
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
```
### 4.3. Define the burn-intent shape
The forwarding flow first estimates the burn-intent limits for the Arc Testnet
to Base Sepolia route, then uses the same burn-intent shape for signing and
submission.
```ts transfer-from-evm.ts theme={null}
type TransferSpecBytes32 = {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: `0x${string}`;
destinationContract: `0x${string}`;
sourceToken: `0x${string}`;
destinationToken: `0x${string}`;
sourceDepositor: `0x${string}`;
destinationRecipient: `0x${string}`;
sourceSigner: `0x${string}`;
destinationCaller: `0x${string}`;
value: bigint;
salt: `0x${string}`;
hookData: `0x${string}`;
};
```
Use a small helper layer to build the bytes32 transfer spec and normalize EVM
addresses for the Gateway payload.
```ts transfer-from-evm.ts theme={null}
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
function createTransferSpec(): TransferSpecBytes32 {
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destConfig = chainConfig[DEST_CHAIN];
return {
version: 1,
sourceDomain: sourceConfig.domainId,
destinationDomain: destConfig.domainId,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(sourceConfig.usdcAddress),
destinationToken: addressToBytes32(destConfig.usdcAddress),
sourceDepositor: addressToBytes32(account.address),
destinationRecipient: addressToBytes32(account.address),
sourceSigner: addressToBytes32(account.address),
destinationCaller: addressToBytes32(zeroAddress),
value: TRANSFER_VALUE,
salt: ("0x" + randomBytes(32).toString("hex")) as Hex,
hookData: "0x",
};
}
function addressToBytes32(address: string): Hex {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
```
### 4.4. Estimate forwarding fees
Before you submit the forwarding transfer, call the Gateway estimate endpoint
with `enableForwarder=true`. The response returns the `maxFee` and
`maxBlockHeight` values you must include in the signed burn intent.
```ts transfer-from-evm.ts theme={null}
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
```
### 4.5. Sign and submit the forwarding burn intent
Sign the burn intent with the Arc Testnet self-managed wallet, then submit it to
the Gateway transfer endpoint with `enableForwarder=true`.
```ts transfer-from-evm.ts theme={null}
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: { maxBlockHeight, maxFee, spec },
};
const signature = await account.signTypedData(typedData);
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Base Sepolia mint for you. Poll the
transfer status endpoint until the transfer is finalized or fails.
```ts transfer-from-evm.ts theme={null}
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
```
### 4.7. Full EVM forwarding script (Self-managed)
The script estimates forwarding fees, signs the burn intent on Arc Testnet,
submits it to the Forwarding Service, and polls until Circle completes the Base
Sepolia mint. Inline comments explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import { pad, zeroAddress, formatUnits, type Hex } from "viem";
import { randomBytes } from "node:crypto";
import {
account,
chainConfig,
GATEWAY_API_BASE,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
stringifyBigInts,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const SOURCE_CHAIN = "arc" as const;
const DEST_CHAIN = "base" as const;
// EIP-712 Domain and Types for Gateway burn intents
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
async function main() {
const sourceConfig = chainConfig[SOURCE_CHAIN];
const destConfig = chainConfig[DEST_CHAIN];
console.log(`Using account: ${account.address}`);
console.log(
`Forwarding from: ${sourceConfig.chain.name} -> ${destConfig.chain.name}`,
);
const spec = {
version: 1,
sourceDomain: sourceConfig.domainId,
destinationDomain: destConfig.domainId,
sourceContract: pad(GATEWAY_WALLET_ADDRESS.toLowerCase() as Hex, {
size: 32,
}),
destinationContract: pad(GATEWAY_MINTER_ADDRESS.toLowerCase() as Hex, {
size: 32,
}),
sourceToken: pad(sourceConfig.usdcAddress.toLowerCase() as Hex, {
size: 32,
}),
destinationToken: pad(destConfig.usdcAddress.toLowerCase() as Hex, {
size: 32,
}),
sourceDepositor: pad(account.address.toLowerCase() as Hex, { size: 32 }),
destinationRecipient: pad(account.address.toLowerCase() as Hex, {
size: 32,
}),
sourceSigner: pad(account.address.toLowerCase() as Hex, { size: 32 }),
destinationCaller: pad(zeroAddress as Hex, { size: 32 }),
value: TRANSFER_VALUE,
salt: ("0x" + randomBytes(32).toString("hex")) as Hex,
hookData: "0x" as Hex,
};
// [1] Estimate forwarding fee inputs for the burn intent.
console.log("\n[1/3] Estimating forwarding fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimated = estimateResult.body[0]?.burnIntent;
if (!estimated) throw new Error("Missing burnIntent estimate");
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(
`Forwarding fee: ${Number(fees.forwardingFee) / 1_000_000} ${fees.token}`,
);
}
console.log(`Estimated maxFee: ${formatUnits(maxFee, 6)} ${fees.token}`);
// [2] Sign and submit the forwarding burn intent.
console.log("\n[2/3] Signing and submitting burn intent...");
const typedData = {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain,
primaryType: "BurnIntent" as const,
message: { maxBlockHeight, maxFee, spec },
};
const signature = await account.signTypedData(typedData);
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = (await response.json()) as TransferResponse;
if (!json.transferId) throw new Error("Missing transferId in response");
const transferId = json.transferId;
console.log(`Transfer ID: ${transferId}`);
// [3] Poll until the Forwarding Service completes the destination mint.
console.log("\n[3/3] Polling for transfer completion...");
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferId}`,
);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollRes.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
if (details.transactionHash) {
console.log(`Mint transaction hash: ${details.transactionHash}`);
}
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
if (Date.now() - pollStart >= POLL_TIMEOUT_MS) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`Transfer complete. ${formatUnits(TRANSFER_VALUE, 6)} USDC forwarded to ${destConfig.chain.name}.`,
);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the EVM forwarding script
Run the script to burn from your Arc Testnet Gateway balance and let the
Forwarding Service mint on Base Sepolia.
Confirm these values before running:
* `EVM_PRIVATE_KEY` controls the source Arc Testnet account
* the source account has a Gateway balance on Arc Testnet
* no destination gas is required for this forwarding path
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the transfer ID, status, and
destination mint transaction hash.
## Step 4. Transfer USDC from Solana to EVM
This step transfers USDC from your Solana Devnet Gateway balance to Arc Testnet.
Both paths create and sign the Solana burn intent first. Direct Mint then
requests the Gateway attestation and calls `gatewayMint(...)` on Arc Testnet
from your wallet, while the Forwarding Service lets Circle complete the
destination mint for you.
### 4.1. Create the script file
You can skip to the
[full transfer script](#4-7-full-solana-transfer-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the destination chain.
This direct-mint flow signs the burn intent on Solana Devnet, requests the
Gateway attestation, and then calls `gatewayMint(...)` on Arc Testnet from your
wallet.
```ts transfer-from-sol.ts theme={null}
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
// Custom layout for Solana PublicKey values in the burn intent payload.
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers.
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const gatewayMinterAbi = [
{
type: "function",
name: "gatewayMint",
inputs: [
{ name: "attestationPayload", type: "bytes" },
{ name: "signature", type: "bytes" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
```
### 4.3. Add helper functions
The helper layer encodes the Solana burn intent into the binary layout expected
by Gateway, then normalizes addresses and payloads for the API request.
```ts transfer-from-sol.ts theme={null}
function encodeBurnIntent(bi: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: string;
destinationContract: string;
sourceToken: string;
destinationToken: string;
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
destinationCaller: string;
value: bigint;
salt: string;
hookData: string;
};
}): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function signBurnIntent(
keypair: Keypair,
payload: Parameters[0],
): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
function hexToPublicKey(hexString: string): PublicKey {
const cleanHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString;
return new PublicKey(Buffer.from(cleanHex, "hex"));
}
```
### 4.4. Create and sign burn intent
Build the Solana burn intent, prefix the encoded payload, and sign it with the
source Solana self-managed wallet.
For Solana-origin transfers, the burn intent uses Solana-specific binary
encoding and must be signed with the Solana signing-domain prefix. See the
[Solana technical guide](/gateway/references/solana#signing) for the signing
rules and message format details.
```ts transfer-from-sol.ts theme={null}
const destConfig = chainConfig[DEST_CHAIN];
const burnIntent = {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: destConfig.domainId,
sourceContract: solanaAddressToBytes32(SOLANA_GATEWAY_WALLET),
destinationContract: evmAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(SOLANA_USDC),
destinationToken: evmAddressToBytes32(destConfig.usdcAddress),
sourceDepositor: solanaAddressToBytes32(solanaKeypair.publicKey.toBase58()),
destinationRecipient: evmAddressToBytes32(account.address),
sourceSigner: solanaAddressToBytes32(solanaKeypair.publicKey.toBase58()),
destinationCaller: evmAddressToBytes32(zeroAddress),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
},
};
const signature = signBurnIntent(solanaKeypair, burnIntent);
const request = [{ burnIntent, signature }];
```
### 4.5. Request attestation from Gateway API
```ts transfer-from-sol.ts theme={null}
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts(request),
});
```
### 4.6. Mint on Arc Testnet
Use the destination EVM self-managed wallet to call `gatewayMint(bytes,bytes)`
on Arc Testnet, then wait for completion.
```ts transfer-from-sol.ts theme={null}
const destConfig = chainConfig[DEST_CHAIN];
const destClient = createPublicClient({
chain: destConfig.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: destConfig.chain,
transport: http(),
});
const destinationGatewayMinterContract = getContract({
address: GATEWAY_MINTER_ADDRESS,
abi: gatewayMinterAbi,
client: { public: destClient, wallet: walletClient },
});
const mintTx = await destinationGatewayMinterContract.write.gatewayMint(
[attestation, operatorSig],
{ account },
);
```
### 4.7. Full Solana transfer script (Self-managed)
The complete transfer script signs a Solana burn intent, requests a Gateway
attestation, and mints on Arc Testnet. Inline comments explain each stage.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import * as crypto from "crypto";
import {
createPublicClient,
createWalletClient,
getContract,
http,
pad,
zeroAddress,
formatUnits,
type Hex,
} from "viem";
import { Keypair, PublicKey } from "@solana/web3.js";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import bs58 from "bs58";
import {
account,
chainConfig,
GATEWAY_API_BASE,
GATEWAY_MINTER_ADDRESS,
stringifyBigInts,
} from "./config.js";
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const DEST_CHAIN = "arc" as const;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
// Custom layout for Solana PublicKey values in the burn intent payload.
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers.
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const gatewayMinterAbi = [
{
type: "function",
name: "gatewayMint",
inputs: [
{ name: "attestationPayload", type: "bytes" },
{ name: "signature", type: "bytes" },
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
function encodeBurnIntent(bi: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: string;
destinationContract: string;
sourceToken: string;
destinationToken: string;
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
destinationCaller: string;
value: bigint;
salt: string;
hookData: string;
};
}): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function signBurnIntent(
keypair: Keypair,
payload: Parameters[0],
): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
function hexToPublicKey(hexString: string): PublicKey {
const cleanHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString;
return new PublicKey(Buffer.from(cleanHex, "hex"));
}
async function main() {
const destConfig = chainConfig[DEST_CHAIN];
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR not set");
}
const solanaKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.SOLANA_PRIVATE_KEYPAIR)),
);
console.log(`Sender (Solana): ${solanaKeypair.publicKey.toBase58()}`);
console.log(`Recipient (EVM): ${account.address}`);
console.log(`Transferring from: Solana Devnet -> ${destConfig.chain.name}`);
// [1] Create and sign the Solana burn intent.
console.log(
`Creating burn intent from Solana Devnet -> ${destConfig.chain.name}...`,
);
const burnIntent = {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: destConfig.domainId,
sourceContract: solanaAddressToBytes32(SOLANA_GATEWAY_WALLET),
destinationContract: evmAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(SOLANA_USDC),
destinationToken: evmAddressToBytes32(destConfig.usdcAddress),
sourceDepositor: solanaAddressToBytes32(
solanaKeypair.publicKey.toBase58(),
),
destinationRecipient: evmAddressToBytes32(account.address),
sourceSigner: solanaAddressToBytes32(solanaKeypair.publicKey.toBase58()),
destinationCaller: evmAddressToBytes32(zeroAddress),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
const signature = signBurnIntent(solanaKeypair, burnIntent);
const request = [{ burnIntent, signature }];
console.log("Signed burn intent.");
// [2] Request the attestation set from Gateway API.
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts(request),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = await response.json();
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const attestation = json?.attestation;
const operatorSig = json?.signature;
if (!attestation || !operatorSig) {
throw new Error("Missing attestation or signature in response");
}
// [3] Mint on Arc Testnet with the returned attestation set.
console.log(`Minting funds on ${destConfig.chain.name}...`);
const destClient = createPublicClient({
chain: destConfig.chain,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: destConfig.chain,
transport: http(),
});
const destinationGatewayMinterContract = getContract({
address: GATEWAY_MINTER_ADDRESS,
abi: gatewayMinterAbi,
client: { public: destClient, wallet: walletClient },
});
const mintTx = await destinationGatewayMinterContract.write.gatewayMint(
[attestation, operatorSig],
{ account },
);
await destClient.waitForTransactionReceipt({ hash: mintTx });
console.log(`Minted ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log(`Mint transaction hash (${destConfig.chain.name}):`, mintTx);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the Solana to EVM direct-mint script
Run the script to burn from your Solana Devnet Gateway balance and call
`gatewayMint(...)` on Arc Testnet.
Confirm these values before running:
* `SOLANA_PRIVATE_KEYPAIR` controls the source Solana Devnet wallet
* `EVM_PRIVATE_KEY` controls the destination Arc Testnet wallet
* the source Solana wallet has a Gateway balance on Solana Devnet
* the destination Arc Testnet wallet can submit the mint transaction
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the minted amount and mint
transaction hash.
### 4.1. Create the script file
You can skip to the
[full transfer script](#4-7-full-solana-forwarding-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
This script uses Arc Testnet as the destination chain.
This forwarding flow signs the burn intent on Solana Devnet, submits it with
forwarding enabled, and then lets Circle complete the destination mint on Arc
Testnet.
```ts transfer-from-sol.ts theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
```
### 4.3. Add helper functions
The helper layer encodes the Solana burn intent into the binary layout expected
by Gateway, then normalizes addresses and payloads for the API request.
```ts transfer-from-sol.ts theme={null}
function encodeBurnIntent(bi: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: string;
destinationContract: string;
sourceToken: string;
destinationToken: string;
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
destinationCaller: string;
value: bigint;
salt: string;
hookData: string;
};
}): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function signBurnIntent(
keypair: Keypair,
payload: Parameters[0],
): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
function hexToPublicKey(hexString: string): PublicKey {
const cleanHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString;
return new PublicKey(Buffer.from(cleanHex, "hex"));
}
```
### 4.4. Estimate forwarding fees
Before you submit the forwarding transfer, call the Gateway estimate endpoint
with `enableForwarder=true`. The response returns the `maxFee` and
`maxBlockHeight` values you must include in the signed burn intent.
```ts transfer-from-sol.ts theme={null}
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
```
### 4.5. Sign and submit the forwarding burn intent
Create the Solana burn intent with the estimated values, prefix the encoded
payload, and sign it with the source Solana self-managed wallet. Then submit it
to the Gateway transfer endpoint with `enableForwarder=true`.
For Solana-origin transfers, the burn intent uses Solana-specific binary
encoding and must be signed with the Solana signing-domain prefix. See the
[Solana technical guide](/gateway/references/solana#signing) for the signing
rules and message format details.
```ts transfer-from-sol.ts theme={null}
const burnIntent = { maxBlockHeight, maxFee, spec };
const signature = signBurnIntent(solanaKeypair, burnIntent);
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Arc Testnet mint for you. Poll the transfer
status endpoint until the transfer is finalized or fails.
```ts transfer-from-sol.ts theme={null}
const pollRes = await fetch(`${GATEWAY_API_BASE}/v1/transfer/${transferId}`);
```
### 4.7. Full Solana forwarding script (Self-managed)
The script estimates forwarding fees, signs the burn intent on Solana Devnet,
submits it to the Forwarding Service, and polls until Circle completes the Arc
Testnet mint. Inline comments explain each stage.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import * as crypto from "crypto";
import { pad, zeroAddress, formatUnits, type Hex } from "viem";
import { Keypair, PublicKey } from "@solana/web3.js";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import bs58 from "bs58";
import {
account,
chainConfig,
GATEWAY_API_BASE,
GATEWAY_MINTER_ADDRESS,
stringifyBigInts,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const SOLANA_GATEWAY_WALLET = "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
const SOLANA_USDC = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const SOLANA_DOMAIN = 5;
const DEST_CHAIN = "arc" as const;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
function encodeBurnIntent(bi: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: {
version: number;
sourceDomain: number;
destinationDomain: number;
sourceContract: string;
destinationContract: string;
sourceToken: string;
destinationToken: string;
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
destinationCaller: string;
value: bigint;
salt: string;
hookData: string;
};
}): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
function signBurnIntent(
keypair: Keypair,
payload: Parameters[0],
): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
function evmAddressToBytes32(address: string): string {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
function hexToPublicKey(hexString: string): PublicKey {
const cleanHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString;
return new PublicKey(Buffer.from(cleanHex, "hex"));
}
async function main() {
const destConfig = chainConfig[DEST_CHAIN];
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR not set");
}
const solanaKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.SOLANA_PRIVATE_KEYPAIR)),
);
console.log(`Sender (Solana): ${solanaKeypair.publicKey.toBase58()}`);
console.log(`Recipient (EVM): ${account.address}`);
console.log(`Forwarding from: Solana Devnet -> ${destConfig.chain.name}`);
const spec = {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: destConfig.domainId,
sourceContract: solanaAddressToBytes32(SOLANA_GATEWAY_WALLET),
destinationContract: evmAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(SOLANA_USDC),
destinationToken: evmAddressToBytes32(destConfig.usdcAddress),
sourceDepositor: solanaAddressToBytes32(solanaKeypair.publicKey.toBase58()),
destinationRecipient: evmAddressToBytes32(account.address),
sourceSigner: solanaAddressToBytes32(solanaKeypair.publicKey.toBase58()),
destinationCaller: evmAddressToBytes32(zeroAddress),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
};
// [1] Estimate forwarding fee inputs for the burn intent.
console.log("\n[1/3] Estimating forwarding fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimated = estimateResult.body[0]?.burnIntent;
if (!estimated) throw new Error("Missing burnIntent estimate");
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(`Forwarding fee: ${fees.forwardingFee} ${fees.token}`);
}
console.log(`Estimated maxFee: ${formatUnits(maxFee, 6)} ${fees.token}`);
// [2] Create, sign, and submit the forwarding burn intent.
console.log("\n[2/3] Signing and submitting burn intent...");
const burnIntent = { maxBlockHeight, maxFee, spec };
const signature = signBurnIntent(solanaKeypair, burnIntent);
const response = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent, signature }]),
},
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gateway API error: ${response.status} ${text}`);
}
const json = (await response.json()) as TransferResponse;
if (!json.transferId) throw new Error("Missing transferId in response");
const transferId = json.transferId;
console.log(`Transfer ID: ${transferId}`);
// [3] Poll until the Forwarding Service completes the destination mint.
console.log("\n[3/3] Polling for transfer completion...");
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollRes = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferId}`,
);
if (!pollRes.ok) {
console.error(`Poll error: ${pollRes.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollRes.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
if (details.transactionHash) {
console.log(`Mint transaction hash: ${details.transactionHash}`);
}
break;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
if (Date.now() - pollStart >= POLL_TIMEOUT_MS) {
throw new Error("Polling timed out waiting for transfer completion");
}
console.log(
`Transfer complete. ${formatUnits(TRANSFER_VALUE, 6)} USDC forwarded to ${destConfig.chain.name}.`,
);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the Solana forwarding script
Run the script to burn from your Solana Devnet Gateway balance and let the
Forwarding Service mint on Arc Testnet.
Confirm these values before running:
* `SOLANA_PRIVATE_KEYPAIR` controls the source Solana Devnet wallet
* `EVM_PRIVATE_KEY` controls the destination Arc Testnet wallet
* the source Solana wallet has a Gateway balance on Solana Devnet
* no destination gas is required for this forwarding path
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the transfer ID, status, and
destination mint transaction hash.
# Quickstart: Create and transfer a unified USDC balance on Solana
Source: https://developers.circle.com/gateway/quickstarts/unified-balance-solana
Create and transfer a unified USDC balance on Solana using Circle Gateway.
This guide walks you through the process of creating Unified Crosschain USDC
Balances on Solana using Circle Gateway, and performing transfers from EVM to
Solana and from Solana to Solana.
**Use
[Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit)
to simplify this integration.**
This quickstart uses a manual Gateway integration. It is for learning or for
developers who need direct control.
To simplify, use Unified Balance Kit to deposit and spend USDC in just a few
lines of code.
Select a tab below for the Circle Wallets or self-managed wallet path.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Obtained a
[Circle API Key](/w3s/circle-developer-account#creating-an-api-key-for-developer-services)
and [Entity Secret](/wallets/dev-controlled/register-entity-secret) from the
[Circle Console](/w3s/circle-developer-account).
* Created Solana Devnet
[developer-controlled wallets](/wallets/dev-controlled/create-your-first-wallet)
for the sender and recipient
* Funded your wallet with testnet tokens:
* Get testnet USDC from the [Circle Faucet](https://faucet.circle.com/).
* Get test native tokens from the
[Console Faucet](https://console.circle.com/faucet).
If you want to try the EVM to Solana transfer, ensure that you've:
* Created an Arc Testnet EVM Developer-Controlled Wallet
* Created a Solana Devnet Developer-Controlled Wallet to receive the minted USDC
* Completed the
[deposit flow](/gateway/quickstarts/unified-balance-evm#step-3-deposit-into-a-unified-crosschain-balance-circle-wallets)
from the
[EVM quickstart](/gateway/quickstarts/unified-balance-evm#circle-wallets)
first
### Add testnet funds to your wallet
To interact with Gateway, you need test USDC and native tokens in your wallet on
each chain you deposit from. For direct mints on Solana, the wallet that submits
the Solana transaction also needs SOL to create the recipient Associated Token
Account and call the Gateway Minter program.
You can skip the destination mint transaction by using the [Forwarding
Service](/gateway/howtos/forwarding-service), which completes the Solana mint
for you. The recipient ATA still needs to exist for this quickstart.
Use the [Circle Faucet](https://faucet.circle.com/) to get test USDC. If you
have a [Circle Developer Console](https://console.circle.com) account, you can
use the [Console Faucet](https://console.circle.com/faucet) to get testnet
native tokens. In addition, the following faucets can also be used to fund your
wallet with testnet native tokens:
**Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `arcTestnet` |
| USDC address | `0x3600000000000000000000000000000000000000` |
| Domain ID | `26` |
**Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `avalancheFuji` |
| USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
| Domain ID | `1` |
**Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `baseSepolia` |
| USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Domain ID | `6` |
**Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sepolia` |
| USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Domain ID | `0` |
**Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `hyperliquidEvmTestnet` |
| USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
| Domain ID | `19` |
**Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `seiTestnet` |
| USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
| Domain ID | `16` |
**Faucet:** [Solana Devnet](https://faucet.solana.com/)
| Property | Value |
| ------------ | ------------------------------------------------------- |
| Chain name | `solanaDevnet` (note that Solana is not EVM-compatible) |
| USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` |
| Domain ID | `5` |
**Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sonicTestnet` |
| USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
| Domain ID | `13` |
**Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `worldchainSepolia` |
| USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
| Domain ID | `14` |
## Step 1. Set up your project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir unified-gateway-balance-solana-circle-wallets
cd unified-gateway-balance-solana-circle-wallets
npm init -y
# Set up module type and run scripts
npm pkg set type=module
npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
npm pkg set scripts.balances="node --env-file=.env balances.ts"
npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"
# Pin bigint-buffer to a patched version
npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
# Install runtime dependencies
npm install @circle-fin/developer-controlled-wallets @coral-xyz/anchor @solana/buffer-layout @solana/spl-token @solana/web3.js bs58 bn.js typescript
# Install dev dependencies
npm install --save-dev @types/node @types/bn.js
```
### 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
Create a `.env` file in the project directory:
```text .env theme={null}
CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
DEPOSITOR_ADDRESS=YOUR_SOURCE_WALLET_ADDRESS
RECIPIENT_ADDRESS=YOUR_DESTINATION_WALLET_ADDRESS
```
* `CIRCLE_API_KEY` is your Circle API key.
* `CIRCLE_ENTITY_SECRET` is your Circle entity secret.
* `DEPOSITOR_ADDRESS` is the source depositor wallet for the script you are
running.
* `RECIPIENT_ADDRESS` is the destination wallet that receives the minted USDC.
For `transfer-from-sol.ts`, both values are Solana addresses.
For `transfer-from-evm.ts`, `DEPOSITOR_ADDRESS` is an EVM address and
`RECIPIENT_ADDRESS` is a Solana address.
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.
## Step 2. Set up the configuration file
The shared Solana configuration and helpers are used by the deposit and transfer
scripts.
### 2.1. Create the configuration file
```shell theme={null}
touch config.ts
```
### 2.2. Configure Solana settings and Gateway helpers
Add the shared Solana RPC configuration, Gateway addresses, IDLs, attestation
decoding helpers, and Circle Wallets signing helpers to `config.ts`.
The local IDL fragments in this example are only the subset needed by the sample
code. For canonical static instruction and account definitions, use the onchain
IDLs linked from
[Solana programs and interfaces](/gateway/references/solana-programs#account-structures).
```ts config.ts expandable theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import {
u32be,
nu64be,
struct,
seq,
blob,
offset,
Layout,
} from "@solana/buffer-layout";
import bs58 from "bs58";
export const RPC_ENDPOINT = "https://api.devnet.solana.com";
export const SOLANA_DOMAIN = 5;
export const SOLANA_ZERO_ADDRESS = "11111111111111111111111111111111";
export const GATEWAY_WALLET_ADDRESS =
"GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
export const GATEWAY_MINTER_ADDRESS =
"GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr";
export const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
const API_KEY = process.env.CIRCLE_API_KEY!;
const ENTITY_SECRET = process.env.CIRCLE_ENTITY_SECRET!;
if (!API_KEY || !ENTITY_SECRET) {
console.error(
"Missing required env vars: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET",
);
process.exit(1);
}
export const client = initiateDeveloperControlledWalletsClient({
apiKey: API_KEY,
entitySecret: ENTITY_SECRET,
});
export class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
export const publicKey = (property: string) => new PublicKeyLayout(property);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
// Sample-local IDL subset for this example.
export const gatewayWalletIdl = {
address: GATEWAY_WALLET_ADDRESS,
metadata: {
name: "gatewayWallet",
version: "0.1.0",
spec: "0.1.0",
},
instructions: [
{
name: "deposit",
discriminator: [22, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "owner", signer: true },
{ name: "gatewayWallet" },
{ name: "ownerTokenAccount", writable: true },
{ name: "custodyTokenAccount", writable: true },
{ name: "deposit", writable: true },
{ name: "depositorDenylist" },
{ name: "tokenProgram" },
{ name: "systemProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [{ name: "amount", type: "u64" }],
},
],
};
// Sample-local IDL subset for this example.
export const gatewayMinterIdl = {
address: GATEWAY_MINTER_ADDRESS,
metadata: { name: "gatewayMinter", version: "0.1.0", spec: "0.1.0" },
instructions: [
{
name: "gatewayMint",
discriminator: [12, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "destinationCaller", signer: true },
{ name: "gatewayMinter" },
{ name: "systemProgram" },
{ name: "tokenProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [
{
name: "params",
type: { defined: { name: "gatewayMintParams" } },
},
],
},
],
types: [
{
name: "gatewayMintParams",
type: {
kind: "struct",
fields: [
{ name: "attestation", type: "bytes" },
{ name: "signature", type: "bytes" },
],
},
},
],
};
export function findDepositPDAs(
programId: PublicKey,
usdcMint: PublicKey,
owner: PublicKey,
) {
return {
wallet: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_wallet")],
programId,
)[0],
custody: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_wallet_custody"), usdcMint.toBuffer()],
programId,
)[0],
deposit: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
programId,
)[0],
denylist: PublicKey.findProgramAddressSync(
[Buffer.from("denylist"), owner.toBuffer()],
programId,
)[0],
};
}
export function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
export function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
export function decodeAttestationSet(attestation: string) {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as {
attestations: Array<{
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
}>;
};
}
export function solanaAddressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
export function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
export async function signAndBroadcast(
circleClient: ReturnType,
connection: Connection,
transaction: Transaction,
walletAddress: string,
label: string,
): Promise {
const serialized = transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
console.log(`Signing ${label} via Circle Wallets...`);
const signResult = await circleClient.signTransaction({
walletAddress,
blockchain: "SOL-DEVNET",
rawTransaction: serialized.toString("base64"),
});
const signedTxBase64 = signResult.data?.signedTransaction;
if (!signedTxBase64) throw new Error(`Failed to sign ${label}`);
console.log(`Broadcasting ${label}...`);
const signedTxBytes = Buffer.from(signedTxBase64, "base64");
return connection.sendRawTransaction(signedTxBytes);
}
export function stringifyTypedData(obj: T) {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
```
## Step 3. Deposit into a unified crosschain balance (Circle Wallets)
The deposit script submits a Gateway deposit on Solana Devnet. You can skip to
the [full deposit script](#3-5-full-deposit-script-circle-wallets) if you
prefer.
Do not send USDC directly to the Gateway Wallet address or custody account.
You must use a Gateway deposit instruction for the funds to be credited to
your unified balance.
### 3.1. Create the deposit script
```shell theme={null}
touch deposit.ts
```
### 3.2. Define constants and helpers
Set the deposit amount near the top of the file, then derive the owner ATA and
load the account so the script can validate balance before it builds the Gateway
instruction.
```ts theme={null}
const DEPOSIT_AMOUNT = new BN(1_000_000);
```
### 3.3. Initialize connection, Anchor client, and validate balance
Initialize the connection, check the source wallet balance, then set up the
Anchor client and derive the Gateway PDAs.
```ts theme={null}
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
const owner = new PublicKey(DEPOSITOR_ADDRESS);
const userTokenAccount = getAssociatedTokenAddressSync(usdcMint, owner);
const tokenAccountInfo = await getAccount(connection, userTokenAccount);
const dummyWallet = new Wallet(Keypair.generate());
const provider = new AnchorProvider(
connection,
dummyWallet,
AnchorProvider.defaultOptions(),
);
```
### 3.4. Execute the deposit
After the balance check and PDA derivation, build the Gateway deposit
instruction, sign it with Circle Wallets, broadcast it, and wait for Solana
confirmation.
```ts theme={null}
const depositIx = await program.methods
.deposit(DEPOSIT_AMOUNT)
.accountsPartial({
payer: owner,
owner: owner,
gatewayWallet: pdas.wallet,
ownerTokenAccount: userTokenAccount,
custodyTokenAccount: pdas.custody,
deposit: pdas.deposit,
depositorDenylist: pdas.denylist,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.instruction();
const txSignature = await signAndBroadcast(
client,
connection,
transaction,
DEPOSITOR_ADDRESS,
"deposit",
);
```
### 3.5. Full deposit script (Circle Wallets)
The script validates the source balance, builds the Gateway deposit instruction,
and confirms the deposit on Solana Devnet. Inline comments explain each stage.
```ts deposit.ts expandable theme={null}
import {
Wallet,
AnchorProvider,
setProvider,
Program,
} from "@coral-xyz/anchor";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
getAssociatedTokenAddressSync,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import BN from "bn.js";
import {
RPC_ENDPOINT,
GATEWAY_WALLET_ADDRESS,
USDC_ADDRESS,
client,
gatewayWalletIdl,
findDepositPDAs,
signAndBroadcast,
} from "./config.js";
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
if (!DEPOSITOR_ADDRESS) {
console.error("Missing required env var: DEPOSITOR_ADDRESS");
process.exit(1);
}
const DEPOSIT_AMOUNT = new BN(1_000_000);
async function main() {
// Set up the Solana connection and core account addresses.
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
const owner = new PublicKey(DEPOSITOR_ADDRESS);
console.log(`Using account: ${owner.toBase58()}`);
console.log(`\n=== Processing Solana Devnet ===`);
// [1] Check the depositor's current USDC balance.
const userTokenAccount = getAssociatedTokenAddressSync(usdcMint, owner);
const tokenAccountInfo = await getAccount(connection, userTokenAccount);
const currentBalance = Number(tokenAccountInfo.amount) / 1_000_000;
console.log(`Current balance: ${currentBalance} USDC`);
if (tokenAccountInfo.amount < BigInt(DEPOSIT_AMOUNT.toString())) {
throw new Error(
"Insufficient USDC balance. Please top up at https://faucet.circle.com",
);
}
// [2] Set up the Anchor client and derive the Gateway deposit PDAs.
const dummyWallet = new Wallet(Keypair.generate());
const provider = new AnchorProvider(
connection,
dummyWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const program = new Program(gatewayWalletIdl, provider);
const pdas = findDepositPDAs(programId, usdcMint, owner);
// [3] Build, sign, and confirm the Gateway deposit transaction.
const depositIx = await program.methods
.deposit(DEPOSIT_AMOUNT)
.accountsPartial({
payer: owner,
owner: owner,
gatewayWallet: pdas.wallet,
ownerTokenAccount: userTokenAccount,
custodyTokenAccount: pdas.custody,
deposit: pdas.deposit,
depositorDenylist: pdas.denylist,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.instruction();
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const transaction = new Transaction();
transaction.add(depositIx);
transaction.recentBlockhash = blockhash;
transaction.feePayer = owner;
const txSignature = await signAndBroadcast(
client,
connection,
transaction,
DEPOSITOR_ADDRESS,
"deposit",
);
await connection.confirmTransaction(
{ signature: txSignature, blockhash, lastValidBlockHeight },
"confirmed",
);
console.log(`Done on Solana Devnet. Deposit tx: ${txSignature}`);
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 3.6. Run the deposit script
```shell theme={null}
npm run deposit
```
Wait for the required number of block confirmations. Once the deposit
transaction is final, your Gateway balance on Solana Devnet will be updated.
Solana Devnet transactions typically reach finality in seconds.
### 3.7. Check the balances on the Gateway Wallet
Create a new file called `balances.ts`, and add the following code. This script
retrieves the USDC balances available from your Gateway Wallet for the
`DEPOSITOR_ADDRESS` currently set in `.env`.
```ts balances.ts expandable theme={null}
interface GatewayBalancesResponse {
balances: Array<{
domain: number;
balance: string;
}>;
}
const EVM_DOMAINS = {
ethereum: 0,
avalanche: 1,
optimism: 2,
arbitrum: 3,
base: 6,
polygon: 7,
unichain: 10,
arc: 26,
};
const SOLANA_DOMAINS = {
solana: 5,
};
const DOMAINS = { ...EVM_DOMAINS, ...SOLANA_DOMAINS };
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
if (!DEPOSITOR_ADDRESS) {
console.error("Missing required env var: DEPOSITOR_ADDRESS");
process.exit(1);
}
const isEvmAddress = DEPOSITOR_ADDRESS.startsWith("0x");
async function main() {
console.log(`Depositor address: ${DEPOSITOR_ADDRESS}`);
console.log(`Address type: ${isEvmAddress ? "EVM" : "Solana"}\n`);
const activeDomains = isEvmAddress ? EVM_DOMAINS : SOLANA_DOMAINS;
const domainIds = Object.values(activeDomains);
const body = {
token: "USDC",
sources: domainIds.map((domain) => ({
domain,
depositor: DEPOSITOR_ADDRESS,
})),
};
const res = await fetch(
"https://gateway-api-testnet.circle.com/v1/balances",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
const result = (await res.json()) as GatewayBalancesResponse;
let total = 0;
for (const balance of result.balances) {
const chain =
Object.keys(DOMAINS).find(
(k) => DOMAINS[k as keyof typeof DOMAINS] === balance.domain,
) || `Domain ${balance.domain}`;
const amount = parseFloat(balance.balance);
console.log(`${chain}: ${amount.toFixed(6)} USDC`);
total += amount;
}
console.log(`\nTotal: ${total.toFixed(6)} USDC`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
You can run it to verify your balance on Gateway.
```shell theme={null}
npm run balances
```
## Step 4. Transfer USDC from Solana to Solana
This step transfers USDC from your Solana Devnet Gateway balance to a recipient
on Solana Devnet. Both paths create and sign the Solana burn intent first.
Direct mint then retrieves the Gateway attestation and calls `gatewayMint(...)`
on Solana Devnet from your wallet, while Forwarding Service lets Circle complete
the Solana mint for you.
### 4.1. Create the Solana transfer script
You can skip to the
[full transfer script](#4-9-full-solana-transfer-script-circle-wallets) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
This flow uses the same Solana burn intent layout as the standard Gateway
quickstart, but swaps in Circle Wallet signing for both the burn intent and the
mint transaction.
```ts theme={null}
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
```
### 4.3. Add helper functions
The helper layer encodes the Solana burn intent, creates a lightweight Anchor
provider, and exposes the address conversion utilities required for Gateway
minting.
```ts theme={null}
function createProvider(connection: Connection) {
const dummyWallet = new Wallet(Keypair.generate());
const provider = new AnchorProvider(
connection,
dummyWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
return provider;
}
```
### 4.4. Initialize connection and create recipient ATA
Before minting to the destination wallet, derive the recipient Associated Token
Account and create it idempotently. In this script, the source
Developer-Controlled Wallet pays for the account creation.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
```ts theme={null}
const provider = createProvider(connection);
const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
const ataTx = new Transaction();
ataTx.add(
createAssociatedTokenAccountIdempotentInstruction(
owner,
recipientAta,
recipientPubkey,
usdcMint,
),
);
```
### 4.5. Create and sign burn intent
Encode the Solana burn intent, prefix the payload, and sign it with the source
Developer-Controlled Wallet.
```ts theme={null}
const burnIntent = createBurnIntent({
sourceDepositor: owner.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: owner.toBase58(),
});
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
```
### 4.6. Request attestation from Gateway API
Submit the signed burn intent to the Gateway API and decode the attestation set
that comes back from the response.
```ts theme={null}
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(request),
},
);
```
### 4.7. Set up minter client
Once the API returns the attestation, initialize the Gateway Minter program and
derive the PDA accounts needed for the Solana mint.
The ordered remaining-account list and PDA derivations are documented in
[Solana Programs and
Interfaces](/gateway/references/solana-programs#gatewaymint). For static
instruction definitions, use the onchain IDLs linked from that page.
```ts theme={null}
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
```
### 4.8. Mint on Solana
Create the mint instruction, sign the transaction with the source
Developer-Controlled Wallet, and confirm it on Solana Devnet.
This script uses the source Solana wallet as both `payer` and
`destinationCaller`. The recipient wallet owns the recipient ATA that receives
the minted USDC.
In this quickstart's client shape, the instruction accounts are assembled as the
fixed `gatewayMint` accounts first, followed by one ordered triplet per
attestation: `custody_token_account`, `destination_recipient`, and
`transfer_spec_hash_account`.
```ts theme={null}
const mintIx = await minterProgram.methods
.gatewayMint({
attestation: attestationBytes,
signature: signatureBytes,
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: owner,
payer: owner,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.instruction();
```
### 4.9. Full Solana transfer script (Circle Wallets)
The script creates the recipient ATA, signs a Solana burn intent, requests a
Gateway attestation, and mints on Solana Devnet. Inline comments explain each
stage.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
} from "@solana/spl-token";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import {
RPC_ENDPOINT,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
USDC_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
client,
gatewayMinterIdl,
publicKey,
hexToPublicKey,
solanaAddressToBytes32,
decodeAttestationSet,
findCustodyPda,
findTransferSpecHashPda,
signAndBroadcast,
stringifyTypedData,
} from "./config.js";
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
// Custom layout for 256-bit unsigned integers.
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
function createBurnIntent(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: solanaAddressToBytes32(sourceDepositor),
destinationRecipient: solanaAddressToBytes32(destinationRecipient),
sourceSigner: solanaAddressToBytes32(sourceSigner),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
// Encode the burn intent into the binary layout expected by Gateway.
function encodeBurnIntent(bi: ReturnType): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
// Create a lightweight Anchor provider for PDA derivation and instruction building.
function createProvider(connection: Connection) {
const dummyWallet = new Wallet(Keypair.generate());
const provider = new AnchorProvider(
connection,
dummyWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
return provider;
}
async function main() {
// Set up the Solana connection and destination accounts.
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const owner = new PublicKey(DEPOSITOR_ADDRESS);
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
console.log(`Using account: ${owner.toBase58()}`);
console.log(`Transferring from: Solana Devnet -> Solana Devnet`);
const provider = createProvider(connection);
// [1] Create the recipient ATA if it does not already exist.
const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
const { blockhash: ataBlockhash, lastValidBlockHeight: ataBlockHeight } =
await connection.getLatestBlockhash();
const ataTx = new Transaction();
ataTx.add(
createAssociatedTokenAccountIdempotentInstruction(
owner,
recipientAta,
recipientPubkey,
usdcMint,
),
);
ataTx.recentBlockhash = ataBlockhash;
ataTx.feePayer = owner;
const ataSig = await signAndBroadcast(
client,
connection,
ataTx,
DEPOSITOR_ADDRESS,
"ATA creation",
);
await connection.confirmTransaction(
{
signature: ataSig,
blockhash: ataBlockhash,
lastValidBlockHeight: ataBlockHeight,
},
"confirmed",
);
// [2] Create and sign the Solana burn intent.
const burnIntent = createBurnIntent({
sourceDepositor: owner.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: owner.toBase58(),
});
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
const burnIntentSignature = sigResult.data?.signature;
if (!burnIntentSignature) throw new Error("Failed to sign burn intent");
const formattedSignature = burnIntentSignature.startsWith("0x")
? burnIntentSignature
: `0x${burnIntentSignature}`;
const request = [{ burnIntent, signature: formattedSignature }];
console.log("Signed burn intent.");
// [3] Request the attestation set from Gateway API.
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData(request),
},
);
const json = (await response.json()) as {
attestation: string;
signature: string;
success?: boolean;
message?: string;
};
if (json.success === false) {
throw new Error(`Gateway API error: ${json.message}`);
}
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
const decoded = decodeAttestationSet(attestation);
// [4] Set up the Gateway Minter client and remaining accounts.
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
const remainingAccounts = decoded.attestations.flatMap((e) => [
{
pubkey: findCustodyPda(e.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{ pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
{
pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
isWritable: true,
isSigner: false,
},
]);
const attestationBytes = Buffer.from(attestation.slice(2), "hex");
const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");
// [5] Mint on Solana with the returned attestation.
console.log("Minting funds on Solana Devnet...");
const mintIx = await minterProgram.methods
.gatewayMint({
attestation: attestationBytes,
signature: signatureBytes,
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: owner,
payer: owner,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.instruction();
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const mintTx = new Transaction();
mintTx.add(mintIx);
mintTx.recentBlockhash = blockhash;
mintTx.feePayer = owner;
const mintSig = await signAndBroadcast(
client,
connection,
mintTx,
DEPOSITOR_ADDRESS,
"mint",
);
await connection.confirmTransaction(
{ signature: mintSig, blockhash, lastValidBlockHeight },
"confirmed",
);
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash (Solana Devnet):`, mintSig);
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.10. Run the Solana direct-mint script
Run the script to burn from your Solana Devnet Gateway balance and mint to the
recipient ATA on Solana Devnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
* `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* the source Solana wallet can submit the ATA creation and mint transactions on
Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the minted amount and Solana Devnet
mint transaction hash.
### 4.1. Create the Solana forwarding script
You can skip to the
[full transfer script](#4-7-full-solana-forwarding-script-circle-wallets) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and response types
This forwarding flow signs the burn intent on Solana Devnet, submits it with
forwarding enabled, and lets Circle complete the Solana Devnet mint.
```ts transfer-from-sol.ts expandable theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
type TransferSpec = ReturnType;
type BurnIntent = {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
};
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Add Solana burn intent helpers
The forwarding flow first estimates the burn-intent limits for the Solana Devnet
to Solana Devnet route, then signs the estimate-provided burn intent with the
source Developer-Controlled Wallet.
```ts transfer-from-sol.ts expandable theme={null}
function createTransferSpec(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: solanaAddressToBytes32(sourceDepositor),
destinationRecipient: solanaAddressToBytes32(destinationRecipient),
sourceSigner: solanaAddressToBytes32(sourceSigner),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
}
function createBurnIntent(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
}): BurnIntent {
return params;
}
```
### 4.4. Estimate forwarding fees
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
Derive the recipient ATA, build the transfer spec, then call the Gateway
estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
and `maxBlockHeight` values to include in the signed burn intent.
```ts transfer-from-sol.ts theme={null}
const owner = new PublicKey(DEPOSITOR_ADDRESS);
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientPubkey,
);
const spec = createTransferSpec({
sourceDepositor: owner.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: owner.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
```
### 4.5. Sign and submit the forwarding burn intent
Create the burn intent with the estimate-provided limits, sign it with the
source Developer-Controlled Wallet, then submit it to the Gateway transfer
endpoint with `enableForwarder=true`.
```ts transfer-from-sol.ts theme={null}
const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
const signature = await signBurnIntent(burnIntent);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Solana Devnet mint for you. Poll the
transfer status endpoint until the transfer is confirmed, finalized, failed, or
expired.
```ts transfer-from-sol.ts theme={null}
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
```
### 4.7. Full Solana forwarding script (Circle Wallets)
The complete script estimates forwarding fees, signs the burn intent on Solana
Devnet, submits it with forwarding enabled, and polls until Circle completes the
Solana Devnet mint.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
import {
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
USDC_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
client,
publicKey,
hexToPublicKey,
solanaAddressToBytes32,
stringifyTypedData,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
type TransferSpec = ReturnType;
type BurnIntent = {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
};
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
/* Transfer parameters */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
/* Custom layout for 256-bit unsigned integers (burn intent encoding only) */
// @solana/buffer-layout's TypeScript definitions are incomplete (archived Jan 2025)
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
return b.subarray(offset, offset + 32).readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
/* Burn intent binary layout */
// 'as any' needed due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
/* Helpers */
function createTransferSpec(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: solanaAddressToBytes32(sourceDepositor),
destinationRecipient: solanaAddressToBytes32(destinationRecipient),
sourceSigner: solanaAddressToBytes32(sourceSigner),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
}
function createBurnIntent(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
}): BurnIntent {
return params;
}
function encodeBurnIntent(burnIntent: BurnIntent): Buffer {
const hookData = Buffer.from(burnIntent.spec.hookData.slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: burnIntent.maxBlockHeight,
maxFee: burnIntent.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: burnIntent.spec.version,
sourceDomain: burnIntent.spec.sourceDomain,
destinationDomain: burnIntent.spec.destinationDomain,
sourceContract: hexToPublicKey(burnIntent.spec.sourceContract),
destinationContract: hexToPublicKey(burnIntent.spec.destinationContract),
sourceToken: hexToPublicKey(burnIntent.spec.sourceToken),
destinationToken: hexToPublicKey(burnIntent.spec.destinationToken),
sourceDepositor: hexToPublicKey(burnIntent.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(
burnIntent.spec.destinationRecipient,
),
sourceSigner: hexToPublicKey(burnIntent.spec.sourceSigner),
destinationCaller: hexToPublicKey(burnIntent.spec.destinationCaller),
value: burnIntent.spec.value,
salt: Buffer.from(burnIntent.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
async function signBurnIntent(burnIntent: BurnIntent): Promise {
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const sigResult = await client.signMessage({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: "SOL-DEVNET",
encodedByHex: true,
message: "0x" + prefixed.toString("hex"),
});
const signature = sigResult.data?.signature;
if (!signature) {
throw new Error("Failed to sign burn intent");
}
return signature.startsWith("0x") ? signature : `0x${signature}`;
}
function formatUsdc(value: bigint): string {
const whole = value / 1_000_000n;
const fraction = (value % 1_000_000n).toString().padStart(6, "0");
return `${whole}.${fraction}`.replace(/\.?0+$/, "");
}
/* Main logic */
async function main() {
const owner = new PublicKey(DEPOSITOR_ADDRESS);
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientPubkey,
);
console.log(`Sender (Solana Devnet): ${owner.toBase58()}`);
console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
console.log(`Amount: ${formatUsdc(TRANSFER_VALUE)} USDC`);
console.log("Forwarding: enabled\n");
// [1] Create transfer spec and estimate forwarding fees
const spec = createTransferSpec({
sourceDepositor: owner.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: owner.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
if (!estimateResponse.ok) {
console.error("Estimate API error status:", estimateResponse.status);
console.error(await estimateResponse.text());
throw new Error("Estimate API request failed");
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
if (!estimatedBurnIntent) {
throw new Error("Estimate API response missing burnIntent");
}
const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
if (estimateResult.fees.forwardingFee) {
console.log(
`Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
);
}
console.log(
`Estimated maxFee: ${formatUsdc(estimatedMaxFee)} ${estimateResult.fees.token}`,
);
console.log(
`Signed maxFee: ${formatUsdc(maxFee)} ${estimateResult.fees.token}`,
);
// [2] Create, sign, and submit burn intent
const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
const signature = await signBurnIntent(burnIntent);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent, signature }]),
},
);
if (!transferResponse.ok) {
console.error("Gateway API error status:", transferResponse.status);
console.error(await transferResponse.text());
throw new Error("Gateway API request failed");
}
const transferResult = (await transferResponse.json()) as TransferResponse;
if (!transferResult.transferId) {
throw new Error("Gateway API response missing transferId");
}
console.log(`Transfer ID: ${transferResult.transferId}`);
// [3] Poll for forwarding completion
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
if (!pollResponse.ok) {
console.error(`Poll error: ${pollResponse.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollResponse.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
console.log(
`Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
);
console.log(`Forwarded ${formatUsdc(TRANSFER_VALUE)} USDC`);
return;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error("Polling timed out waiting for transfer completion");
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the Solana forwarding script
Run the script to burn from your Solana Devnet Gateway balance and let the
Forwarding Service mint to the recipient ATA on Solana Devnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
* `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* the recipient ATA exists on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the transfer ID, status, Solana
Devnet mint transaction hash, and forwarded amount.
## Step 4. Transfer USDC from EVM to Solana
This step transfers USDC from your Arc Testnet Gateway balance to Solana Devnet.
Both paths create and submit the burn intent from Arc Testnet first. Direct mint
then retrieves the Gateway attestation and calls `gatewayMint(...)` on Solana
Devnet from your Solana wallet, while Forwarding Service lets Circle complete
the Solana mint for you.
### 4.1. Create the EVM to Solana transfer script
You can skip to the
[full transfer script](#4-9-full-evm-to-solana-transfer-script-circle-wallets)
if you prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
This flow signs an EIP-712 burn intent on Arc Testnet, requests a Gateway
attestation, and then calls `gatewayMint(...)` on Solana Devnet.
```ts theme={null}
const SOURCE_CHAIN = {
chainName: "Arc Testnet",
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
usdc: "0x3600000000000000000000000000000000000000",
domain: 26,
walletChain: "ARC-TESTNET" as const,
};
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const MAX_UINT64_DEC = MAX_UINT64.toString();
```
### 4.3. Create the burn intent helpers
The burn intent uses EVM `bytes32` values for the Arc Testnet source fields and
Solana `bytes32` values for the destination Gateway Minter, USDC mint, and
recipient ATA.
```ts theme={null}
function createBurnIntent(params: {
depositorAddress: string;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
maxBlockHeight: MAX_UINT64_DEC,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
```
### 4.4. Initialize connection and create recipient ATA
Set up the Solana connection, derive the recipient associated token account
(ATA), and create it idempotently before you request a Gateway attestation.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
```ts theme={null}
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
```
### 4.5. Create and sign the burn intent
Create one burn intent for the Arc Testnet Gateway balance and sign it with the
source Developer-Controlled Wallet.
```ts theme={null}
const burnIntent = createBurnIntent({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAta: recipientAta.toBase58(),
});
const typedData = burnIntentTypedData(burnIntent);
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: SOURCE_CHAIN.walletChain,
data: stringifyTypedData(typedData),
});
```
### 4.6. Request the Gateway attestation
Submit the signed burn intent to the Gateway API and validate that the response
includes both the attestation and operator signature needed for the Solana mint.
```ts theme={null}
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = (await response.json()) as {
attestation?: string;
signature?: string;
};
const { attestation, signature: mintSignature } = json;
if (!attestation || !mintSignature) {
throw new Error("Gateway API response missing attestation or signature");
}
```
### 4.7. Set up minter client
After decoding the attestation set, initialize the Gateway Minter program and
build the remaining account list expected by the Solana mint instruction.
The ordered remaining-account list and PDA derivations are documented in
[Solana Programs and
Interfaces](/gateway/references/solana-programs#gatewaymint). For static
instruction definitions, use the onchain IDLs linked from that page.
```ts theme={null}
const decoded = decodeAttestationSet(attestation);
const minterProgram = new Program(gatewayMinterIdl, provider);
const remainingAccounts = decoded.attestations.flatMap((e) => [
{
pubkey: findCustodyPda(e.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{ pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
{
pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
isWritable: true,
isSigner: false,
},
]);
```
### 4.8. Mint on Solana
Create the Solana mint instruction, sign it with the destination
Developer-Controlled Wallet, then confirm the transaction on Solana Devnet.
This script sets `destinationCaller` to the Solana zero address in the burn
intent, so the recipient wallet can submit the mint as both `payer` and
`destinationCaller`.
```ts theme={null}
const mintIx = await minterProgram.methods
.gatewayMint({
attestation: Buffer.from(attestation.slice(2), "hex"),
signature: Buffer.from(mintSignature.slice(2), "hex"),
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: recipientPubkey,
payer: recipientPubkey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.instruction();
```
### 4.9. Full EVM to Solana transfer script (Circle Wallets)
The script creates the recipient ATA, signs one Arc Testnet burn intent,
requests a Gateway attestation, and mints on Solana Devnet. Inline comments
explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
RPC_ENDPOINT,
GATEWAY_MINTER_ADDRESS,
USDC_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
client,
gatewayMinterIdl,
solanaAddressToBytes32,
decodeAttestationSet,
findCustodyPda,
findTransferSpecHashPda,
signAndBroadcast,
stringifyTypedData,
} from "./config.js";
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
const SOURCE_CHAIN = {
chainName: "Arc Testnet",
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
usdc: "0x3600000000000000000000000000000000000000",
domain: 26,
walletChain: "ARC-TESTNET" as const,
};
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const MAX_UINT64_DEC = MAX_UINT64.toString();
const eip712Domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
];
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
];
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
];
function createBurnIntent(params: {
depositorAddress: string;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
maxBlockHeight: MAX_UINT64_DEC,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: burnIntent,
};
}
function evmAddressToBytes32(address: string): string {
return "0x" + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
function createProvider(connection: Connection) {
const dummyWallet = new Wallet(Keypair.generate());
const provider = new AnchorProvider(
connection,
dummyWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
return provider;
}
async function main() {
console.log(`Sender (${SOURCE_CHAIN.chainName}): ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient (Solana Devnet): ${RECIPIENT_ADDRESS}`);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const provider = createProvider(connection);
// [1] Create recipient's Associated Token Account
const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
console.log(`Recipient ATA: ${recipientAta.toBase58()}`);
const { blockhash: ataBlockhash, lastValidBlockHeight: ataBlockHeight } =
await connection.getLatestBlockhash();
const ataTx = new Transaction();
ataTx.add(
createAssociatedTokenAccountIdempotentInstruction(
recipientPubkey,
recipientAta,
recipientPubkey,
usdcMint,
),
);
ataTx.recentBlockhash = ataBlockhash;
ataTx.feePayer = recipientPubkey;
const ataSig = await signAndBroadcast(
client,
connection,
ataTx,
RECIPIENT_ADDRESS,
"ATA creation",
);
await connection.confirmTransaction(
{
signature: ataSig,
blockhash: ataBlockhash,
lastValidBlockHeight: ataBlockHeight,
},
"confirmed",
);
// [2] Create and sign burn intent
const burnIntent = createBurnIntent({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAta: recipientAta.toBase58(),
});
const typedData = burnIntentTypedData(burnIntent);
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: SOURCE_CHAIN.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) {
throw new Error("Failed to sign burn intent");
}
console.log("Signed burn intent.");
// [3] Request attestation from Gateway API
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = (await response.json()) as {
attestation?: string;
signature?: string;
};
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
if (!attestation || !mintSignature) {
throw new Error("Gateway API response missing attestation or signature");
}
// [4] Set up the minter client
const decoded = decodeAttestationSet(attestation);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
{
pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{
pubkey: attestationItem.destinationRecipient,
isWritable: true,
isSigner: false,
},
{
pubkey: findTransferSpecHashPda(
attestationItem.transferSpecHash,
minterProgramId,
),
isWritable: true,
isSigner: false,
},
]);
const attestationBytes = Buffer.from(attestation.slice(2), "hex");
const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");
// [5] Mint on Solana
console.log("Minting funds on Solana Devnet...");
const mintIx = await minterProgram.methods
.gatewayMint({
attestation: attestationBytes,
signature: signatureBytes,
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: recipientPubkey,
payer: recipientPubkey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.instruction();
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const mintTx = new Transaction();
mintTx.add(mintIx);
mintTx.recentBlockhash = blockhash;
mintTx.feePayer = recipientPubkey;
const mintSig = await signAndBroadcast(
client,
connection,
mintTx,
RECIPIENT_ADDRESS,
"mint",
);
// [6] Wait for confirmation
await connection.confirmTransaction(
{ signature: mintSig, blockhash, lastValidBlockHeight },
"confirmed",
);
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash (Solana Devnet):`, mintSig);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.10. Run the EVM to Solana direct-mint script
Run the script to burn from your Arc Testnet Gateway balance and mint to the
recipient ATA on Solana Devnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
* `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Arc Testnet
* the destination Solana wallet can submit the ATA creation and mint
transactions on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the minted amount and Solana Devnet
mint transaction hash.
### 4.1. Create the EVM to Solana forwarding script
You can skip to the
[full transfer script](#4-7-full-evm-to-solana-forwarding-script-circle-wallets)
if you prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and response types
This forwarding flow signs the burn intent on Arc Testnet, submits it with
forwarding enabled, and lets Circle complete the Solana Devnet mint.
```ts theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Create the transfer spec helpers
The forwarding flow estimates the burn-intent limits for the Arc Testnet to
Solana Devnet route before signing. The transfer spec still identifies the
recipient's Solana ATA as the destination recipient.
```ts theme={null}
function createTransferSpec(params: {
depositorAddress: string;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
}
```
### 4.4. Estimate forwarding fees
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
Call the Gateway estimate endpoint with `enableForwarder=true`. The response
returns the `maxFee` and `maxBlockHeight` values to include in the signed burn
intent.
```ts theme={null}
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientPubkey,
);
const spec = createTransferSpec({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAta: recipientAta.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
if (!estimatedBurnIntent) {
throw new Error("Estimate API response missing burnIntent");
}
const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
```
### 4.5. Sign and submit the forwarding burn intent
Sign the estimated burn intent with the Arc Testnet Developer-Controlled Wallet,
then submit it to the Gateway transfer endpoint with `enableForwarder=true`.
```ts theme={null}
const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: SOURCE_CHAIN.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) {
throw new Error("Failed to sign burn intent");
}
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Solana Devnet mint for you. Poll the
transfer status endpoint until the transfer is confirmed, finalized, failed, or
expired.
```ts theme={null}
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
```
### 4.7. Full EVM to Solana forwarding script (Circle Wallets)
The script estimates forwarding fees, signs the burn intent on Arc Testnet,
submits it with forwarding enabled, and polls until Circle completes the Solana
Devnet mint. Inline comments explain each stage.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import {
GATEWAY_MINTER_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
USDC_ADDRESS,
client,
solanaAddressToBytes32,
stringifyTypedData,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
/* Environment */
const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;
if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
console.error(
"Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
);
process.exit(1);
}
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const SOURCE_CHAIN = {
chainName: "Arc Testnet",
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
usdc: "0x3600000000000000000000000000000000000000",
domain: 26,
walletChain: "ARC-TESTNET" as const,
};
const TRANSFER_AMOUNT = 0.1;
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const eip712Domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
];
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
];
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
];
/* Helpers */
function createTransferSpec(params: {
depositorAddress: string;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
}
function burnIntentTypedData(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: ReturnType;
}) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: params,
};
}
function evmAddressToBytes32(address: string): string {
return "0x" + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
function formatUsdc(value: bigint): string {
const whole = value / 1_000_000n;
const fraction = (value % 1_000_000n).toString().padStart(6, "0");
return `${whole}.${fraction}`.replace(/\.?0+$/, "");
}
/* Main logic */
async function main() {
const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientPubkey,
);
console.log(`Sender (${SOURCE_CHAIN.chainName}): ${DEPOSITOR_ADDRESS}`);
console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
console.log(`Amount: ${formatUsdc(TRANSFER_VALUE)} USDC`);
console.log("Forwarding: enabled\n");
// [1] Create transfer spec and estimate forwarding fees
const spec = createTransferSpec({
depositorAddress: DEPOSITOR_ADDRESS,
recipientAta: recipientAta.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ spec }]),
},
);
if (!estimateResponse.ok) {
console.error("Estimate API error status:", estimateResponse.status);
console.error(await estimateResponse.text());
throw new Error("Estimate API request failed");
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
if (!estimatedBurnIntent) {
throw new Error("Estimate API response missing burnIntent");
}
const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
if (estimateResult.fees.forwardingFee) {
console.log(
`Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
);
}
console.log(
`Estimated maxFee: ${formatUsdc(estimatedMaxFee)} ${estimateResult.fees.token}`,
);
console.log(
`Signed maxFee: ${formatUsdc(maxFee)} ${estimateResult.fees.token}`,
);
// [2] Create, sign, and submit burn intent
const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
const sigResp = await client.signTypedData({
walletAddress: DEPOSITOR_ADDRESS,
blockchain: SOURCE_CHAIN.walletChain,
data: stringifyTypedData(typedData),
});
const signature = sigResp.data?.signature;
if (!signature) {
throw new Error("Failed to sign burn intent");
}
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
},
);
if (!transferResponse.ok) {
console.error("Gateway API error status:", transferResponse.status);
console.error(await transferResponse.text());
throw new Error("Gateway API request failed");
}
const transferResult = (await transferResponse.json()) as TransferResponse;
if (!transferResult.transferId) {
throw new Error("Gateway API response missing transferId");
}
console.log(`Transfer ID: ${transferResult.transferId}`);
// [3] Poll for forwarding completion
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
if (!pollResponse.ok) {
console.error(`Poll error: ${pollResponse.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollResponse.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
console.log(
`Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
);
console.log(`Forwarded ${formatUsdc(TRANSFER_VALUE)} USDC`);
return;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error("Polling timed out waiting for transfer completion");
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the EVM to Solana forwarding script
Run the script to burn from your Arc Testnet Gateway balance and let the
Forwarding Service mint on Solana Devnet.
Confirm these values before running:
* `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
* `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Arc Testnet
* the recipient ATA exists on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the transfer ID, status, Solana
Devnet mint transaction hash, and forwarded amount.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.6+](https://nodejs.org/)
* Prepared Solana Devnet wallets (sender and recipient) and have the private key
pairs exported as JSON arrays
If you want to try the EVM to Solana transfer, ensure that you've:
* Prepared an EVM testnet wallet with the private key available
* Added Arc Testnet to your wallet
* Completed
[Step 3: Deposit into a unified crosschain balance](/gateway/quickstarts/unified-balance-evm#step-3-deposit-into-a-unified-crosschain-balance-self-managed)
from the
[EVM quickstart](/gateway/quickstarts/unified-balance-evm#self-managed)
### Add testnet funds to your wallet
To interact with Gateway, you need test USDC and native tokens in your wallet on
each chain you deposit from. For direct mints on Solana, the wallet that submits
the Solana transaction also needs SOL to create the recipient Associated Token
Account and call the Gateway Minter program.
Use the [Circle Faucet](https://faucet.circle.com/) to get testnet USDC. If you
have a [Circle Developer Console](https://console.circle.com) account, you can
use the [Console Faucet](https://console.circle.com/faucet) to get testnet
native tokens. In addition, the following faucets can also be used to fund your
wallet with testnet native tokens:
**Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `arcTestnet` |
| USDC address | `0x3600000000000000000000000000000000000000` |
| Domain ID | `26` |
**Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `avalancheFuji` |
| USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
| Domain ID | `1` |
**Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `baseSepolia` |
| USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Domain ID | `6` |
**Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sepolia` |
| USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Domain ID | `0` |
**Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `hyperliquidEvmTestnet` |
| USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
| Domain ID | `19` |
**Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `seiTestnet` |
| USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
| Domain ID | `16` |
**Faucet:** [Solana Devnet](https://faucet.solana.com/)
| Property | Value |
| ------------ | ------------------------------------------------------- |
| Chain name | `solanaDevnet` (note that Solana is not EVM-compatible) |
| USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` |
| Domain ID | `5` |
**Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `sonicTestnet` |
| USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
| Domain ID | `13` |
**Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)
| Property | Value |
| ------------ | -------------------------------------------- |
| Chain name | `worldchainSepolia` |
| USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
| Domain ID | `14` |
## Step 1. Set up your project
This step shows you how to prepare your project and environment.
### 1.1. Create the project and install dependencies
Create a new directory and install the required dependencies:
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir unified-gateway-balance-sol
cd unified-gateway-balance-sol
npm init -y
# Set up module type and run scripts
npm pkg set type=module
npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
npm pkg set scripts.balances="node --env-file=.env balances.ts"
# Install dependencies
npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
npm install @coral-xyz/anchor @solana/buffer-layout @solana/spl-token @solana/web3.js bn.js bs58 typescript
npm install --save-dev @types/bn.js @types/node
```
If you want to try the EVM to Solana transfer, add the run script and install
[Viem](https://viem.sh/) :
```shell theme={null}
npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"
npm install viem
```
### 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_KEYPAIR=YOUR_SOLANA_KEYPAIR_ARRAY
RECIPIENT_KEYPAIR=YOUR_RECIPIENT_KEYPAIR_ARRAY
```
* `SOLANA_PRIVATE_KEYPAIR` is the primary Solana wallet keypair as a JSON array.
In `transfer-from-sol.ts`, this is the source sender. In
`transfer-from-evm.ts`, this is the Solana recipient and mint signer.
* `RECIPIENT_KEYPAIR` is the Solana recipient wallet keypair as a JSON array for
`transfer-from-sol.ts`.
If your wallet exports a private key hash instead, you can use `bs58` to convert it:
```ts TypeScript theme={null}
const bytes = bs58.decode("YOUR_PRIVATE_KEY_HASH");
console.log(JSON.stringify(Array.from(bytes)));
```
If you want to try the EVM to Solana transfer, also add:
```text theme={null}
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
```
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use for the EVM
side of the transfer.
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.
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. Set up the configuration file
This section covers the shared configuration file will be used by both the
deposit and transfer scripts.
### 2.1. Create the configuration file
```shell theme={null}
touch config.ts
```
### 2.2. Configure Solana settings and Gateway addresses
Add the Solana-specific configuration, Gateway contract addresses, and account
setup helper to your `config.ts` file. This includes the RPC endpoint, USDC
address, domain ID, and the IDL definitions for interacting with Gateway Wallet
and Gateway Minter programs on Solana Devnet.
The local IDL fragments in this example are only the subset needed by the sample
code. For canonical static instruction and account definitions, use the onchain
IDLs linked from
[Solana programs and interfaces](/gateway/references/solana-programs#account-structures).
```ts config.ts expandable theme={null}
import { Keypair } from "@solana/web3.js";
/* Solana Configuration */
export const RPC_ENDPOINT = "https://api.devnet.solana.com";
export const SOLANA_DOMAIN = 5;
export const SOLANA_ZERO_ADDRESS = "11111111111111111111111111111111";
/* Gateway Contract Addresses */
export const GATEWAY_WALLET_ADDRESS =
"GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
export const GATEWAY_MINTER_ADDRESS =
"GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr";
export const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
/* Account Setup Helper */
export function createKeypairFromEnv(privateKey: string | undefined): Keypair {
if (!privateKey) {
throw new Error("Missing required Solana keypair environment variable");
}
const secretKey = JSON.parse(privateKey);
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
}
/* Sample-local Gateway Wallet IDL subset used by this runnable example. */
export const gatewayWalletIdl = {
address: GATEWAY_WALLET_ADDRESS,
metadata: {
name: "gatewayWallet",
version: "0.1.0",
spec: "0.1.0",
},
instructions: [
{
name: "deposit",
discriminator: [22, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "owner", signer: true },
{ name: "gatewayWallet" },
{ name: "ownerTokenAccount", writable: true },
{ name: "custodyTokenAccount", writable: true },
{ name: "deposit", writable: true },
{ name: "depositorDenylist" },
{ name: "tokenProgram" },
{ name: "systemProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [{ name: "amount", type: "u64" }],
},
],
};
/* Sample-local Gateway Minter IDL subset used by this runnable example. */
export const gatewayMinterIdl = {
address: GATEWAY_MINTER_ADDRESS,
metadata: { name: "gatewayMinter", version: "0.1.0", spec: "0.1.0" },
instructions: [
{
name: "gatewayMint",
discriminator: [12, 0],
accounts: [
{ name: "payer", writable: true, signer: true },
{ name: "destinationCaller", signer: true },
{ name: "gatewayMinter" },
{ name: "systemProgram" },
{ name: "tokenProgram" },
{ name: "eventAuthority" },
{ name: "program" },
],
args: [
{ name: "params", type: { defined: { name: "gatewayMintParams" } } },
],
},
],
types: [
{
name: "gatewayMintParams",
type: {
kind: "struct",
fields: [
{ name: "attestation", type: "bytes" },
{ name: "signature", type: "bytes" },
],
},
},
],
};
```
## Step 3. Deposit into a unified crosschain balance (self-managed)
This section explains parts of the deposit script that allows you to deposit
USDC into the Gateway Wallet contract on Solana Devnet. You can skip to the
[full deposit script](#3-5-full-deposit-script-self-managed) if you prefer.
### 3.1. Create the script file
```shell theme={null}
touch deposit.ts
```
### 3.2. Define constants and helpers
You can adjust the `DEPOSIT_AMOUNT` to a different value. For now, it is set to
10 USDC.
```ts deposit.ts theme={null}
const DEPOSIT_AMOUNT = new BN(10000000); // 10 USDC (6 decimals)
/* Helpers */
function findPDAs(programId: PublicKey, usdcMint: PublicKey, owner: PublicKey) {
return {
wallet: PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_wallet"))],
programId,
)[0],
custody: PublicKey.findProgramAddressSync(
[
Buffer.from(utils.bytes.utf8.encode("gateway_wallet_custody")),
usdcMint.toBuffer(),
],
programId,
)[0],
deposit: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
programId,
)[0],
denylist: PublicKey.findProgramAddressSync(
[Buffer.from("denylist"), owner.toBuffer()],
programId,
)[0],
};
}
```
### 3.3. Initialize connection, Anchor client, and validate balance
Initialize the Solana connection and keypair, set up the Anchor client with
Program Derived Addresses (PDAs) for interacting with the Gateway Wallet
contract, then verify sufficient USDC balance before depositing.
```ts deposit.ts theme={null}
const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
const usdcMint = new PublicKey(USDC_ADDRESS);
console.log(`Using account: ${keypair.publicKey.toBase58()}`);
// Check USDC balance
const userAta = await getAssociatedTokenAddress(usdcMint, keypair.publicKey);
const ataInfo = await getAccount(connection, userAta);
const currentBalance = ataInfo.amount;
console.log(
`Current balance: ${Number(currentBalance.toString()) / 1_000_000} USDC`,
);
if (currentBalance < BigInt(DEPOSIT_AMOUNT.toString())) {
throw new Error(
`Insufficient USDC balance! Please top up at https://faucet.circle.com`,
);
}
const pdas = findPDAs(programId, usdcMint, keypair.publicKey);
const anchorWallet = new Wallet(keypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const program = new Program(gatewayWalletIdl, provider);
```
### 3.4. Execute the deposit
```ts deposit.ts theme={null}
const txHash = await program.methods
.deposit(DEPOSIT_AMOUNT)
.accountsPartial({
payer: keypair.publicKey,
owner: keypair.publicKey,
gatewayWallet: pdas.wallet,
ownerTokenAccount: userAta,
custodyTokenAccount: pdas.custody,
deposit: pdas.deposit,
depositorDenylist: pdas.denylist,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([keypair])
.rpc();
console.log(`Done on Solana Devnet. Deposit tx: ${txHash}`);
```
### 3.5. Full deposit script (Self-managed)
The complete deposit script initializes the Solana connection and Anchor client,
validates the USDC balance, and deposits funds into the Gateway Wallet contract
on Solana Devnet. The script includes inline comments to explain what each
function does, making it easier to follow and modify if needed.
```ts deposit.ts expandable theme={null}
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import { Connection, PublicKey, SystemProgram } from "@solana/web3.js";
import {
getAssociatedTokenAddress,
getAccount,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import BN from "bn.js";
import {
RPC_ENDPOINT,
GATEWAY_WALLET_ADDRESS,
USDC_ADDRESS,
createKeypairFromEnv,
gatewayWalletIdl,
} from "./config.js";
const DEPOSIT_AMOUNT = new BN(10000000); // 10 USDC (6 decimals)
/* Helpers */
function findPDAs(programId: PublicKey, usdcMint: PublicKey, owner: PublicKey) {
return {
wallet: PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_wallet"))],
programId,
)[0],
custody: PublicKey.findProgramAddressSync(
[
Buffer.from(utils.bytes.utf8.encode("gateway_wallet_custody")),
usdcMint.toBuffer(),
],
programId,
)[0],
deposit: PublicKey.findProgramAddressSync(
[Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
programId,
)[0],
denylist: PublicKey.findProgramAddressSync(
[Buffer.from("denylist"), owner.toBuffer()],
programId,
)[0],
};
}
/* Main logic */
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR not set in environment");
}
const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
const usdcMint = new PublicKey(USDC_ADDRESS);
console.log(`Using account: ${keypair.publicKey.toBase58()}`);
console.log(`\n=== Processing Solana Devnet ===`);
// Check USDC balance
const userAta = await getAssociatedTokenAddress(usdcMint, keypair.publicKey);
const ataInfo = await getAccount(connection, userAta);
const currentBalance = ataInfo.amount;
console.log(
`Current balance: ${Number(currentBalance.toString()) / 1_000_000} USDC`,
);
if (currentBalance < BigInt(DEPOSIT_AMOUNT.toString())) {
throw new Error(
`Insufficient USDC balance! Please top up at https://faucet.circle.com`,
);
}
console.log(
`Depositing ${Number(DEPOSIT_AMOUNT.toString()) / 1_000_000} USDC to Gateway Wallet`,
);
// Set up Anchor client
const pdas = findPDAs(programId, usdcMint, keypair.publicKey);
const anchorWallet = new Wallet(keypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const program = new Program(gatewayWalletIdl, provider);
// Execute deposit
const txHash = await program.methods
.deposit(DEPOSIT_AMOUNT)
.accountsPartial({
payer: keypair.publicKey,
owner: keypair.publicKey,
gatewayWallet: pdas.wallet,
ownerTokenAccount: userAta,
custodyTokenAccount: pdas.custody,
deposit: pdas.deposit,
depositorDenylist: pdas.denylist,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([keypair])
.rpc();
console.log(`Done on Solana Devnet. Deposit tx: ${txHash}`);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 3.6. Run the script to create a crosschain balance
Run the deposit script to deposit USDC into your Gateway balance on Solana
Devnet.
```shell theme={null}
npm run deposit
```
Wait for the required number of block confirmations. Once the deposit
transaction is final, your Gateway balance on Solana Devnet will be updated.
Solana Devnet transactions typically reach finality in seconds.
### 3.7. Check the balances on the Gateway Wallet
Create a new file called `balances.ts`, and add the following code. This script
retrieves the USDC balances available from your Gateway Wallet on Solana Devnet.
```ts balances.ts expandable theme={null}
import { Keypair } from "@solana/web3.js";
/* Constants */
const SOLANA_DOMAIN = 5;
/* Helpers */
function createKeypairFromEnv(privateKey: string | undefined): Keypair {
if (!privateKey) {
throw new Error("Missing required Solana keypair environment variable");
}
const secretKey = JSON.parse(privateKey);
return Keypair.fromSecretKey(Uint8Array.from(secretKey));
}
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR not set in environment");
}
const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const depositor = keypair.publicKey.toBase58();
console.log(`Depositor address: ${depositor}\n`);
const body = {
token: "USDC",
sources: [{ domain: SOLANA_DOMAIN, depositor }],
};
const res = await fetch(
"https://gateway-api-testnet.circle.com/v1/balances",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
const result = await res.json();
for (const balance of result.balances) {
const amount = parseFloat(balance.balance);
console.log(`Solana Devnet: ${amount.toFixed(6)} USDC`);
}
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
You can run it to verify your balance on Gateway.
```shell theme={null}
npm run balances
```
## Step 4. Transfer USDC from Solana to Solana
This step transfers USDC from your Solana Devnet Gateway balance to a recipient
on Solana Devnet. Both paths create and sign the Solana burn intent first.
Direct mint then retrieves the Gateway attestation and calls `gatewayMint(...)`
on Solana Devnet from your self-managed Solana wallet, while Forwarding Service
lets Circle complete the Solana mint for you.
### 4.1. Create the script file
You can skip to the
[full transfer script](#4-9-full-solana-transfer-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and types
You can set the amount to be transferred from your Gateway balance by changing
the `TRANSFER_AMOUNT`. For now, it is set to 1 USDC.
```ts transfer-from-sol.ts expandable theme={null}
const TRANSFER_AMOUNT = 1; // 1 USDC
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
/* Type definitions */
// Custom layout for Solana PublicKey (32 bytes)
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
// Type 'as any' used due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
```
### 4.3. Add helper functions
```ts transfer-from-sol.ts expandable theme={null}
// Construct burn intent for a given source
function createBurnIntent(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(USDC_ADDRESS),
destinationToken: addressToBytes32(USDC_ADDRESS),
sourceDepositor: addressToBytes32(sourceDepositor),
destinationRecipient: addressToBytes32(destinationRecipient),
sourceSigner: addressToBytes32(sourceSigner),
destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
// Encode burn intent as binary layout for signing
function encodeBurnIntent(bi: any): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
// Sign burn intent with Ed25519 keypair
function signBurnIntent(keypair: Keypair, payload: any): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
// Convert Solana address to 32-byte hex string
function addressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Convert hex string to Solana PublicKey
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
// Decode attestation set from Gateway API response
function decodeAttestationSet(attestation: string) {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as {
attestations: Array<{
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
}>;
};
}
// Find PDA for token custody account
function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
// Find PDA for transfer spec hash tracking
function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
```
### 4.4. Initialize connection and create recipient ATA
Initialize the Solana connection and keypairs, then create the recipient's
Associated Token Account (ATA) for receiving USDC on the destination chain.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
```ts transfer-from-sol.ts theme={null}
const senderKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
console.log(`Using account: ${senderKeypair.publicKey.toBase58()}`);
console.log(`Transferring balances from: Solana Devnet`);
// Create recipient's Associated Token Account
const recipientAta = getAssociatedTokenAddressSync(
usdcMint,
recipientKeypair.publicKey,
);
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
senderKeypair.publicKey,
recipientAta,
recipientKeypair.publicKey,
usdcMint,
);
const tx = new Transaction().add(createAtaIx);
await sendAndConfirmTransaction(connection, tx, [senderKeypair]);
```
### 4.5. Create and sign burn intent
```ts transfer-from-sol.ts theme={null}
const burnIntent = createBurnIntent({
sourceDepositor: senderKeypair.publicKey.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: senderKeypair.publicKey.toBase58(),
});
const burnIntentSignature = signBurnIntent(senderKeypair, burnIntent);
const request = [{ burnIntent, signature: burnIntentSignature }];
console.log("Signed burn intent.");
```
### 4.6. Request attestation from Gateway API
```ts transfer-from-sol.ts theme={null}
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
const json = await response.json();
if (json.success === false) {
throw new Error(`Gateway API error: ${json.message}`);
}
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
const decoded = decodeAttestationSet(attestation);
```
### 4.7. Set up minter client
The ordered remaining-account list and PDA derivations are documented in
[Solana Programs and
Interfaces](/gateway/references/solana-programs#gatewaymint). For static
instruction definitions, use the onchain IDLs linked from that page.
```ts transfer-from-sol.ts theme={null}
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const anchorWallet = new Wallet(senderKeypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
```
### 4.8. Mint on Solana
```ts transfer-from-sol.ts expandable theme={null}
const remainingAccounts = decoded.attestations.flatMap((e) => [
{
pubkey: findCustodyPda(e.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{ pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
{
pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
isWritable: true,
isSigner: false,
},
]);
const attestationBytes = Buffer.from(attestation.slice(2), "hex");
const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");
console.log("Minting funds on Solana Devnet...");
const mintTx = await minterProgram.methods
.gatewayMint({ attestation: attestationBytes, signature: signatureBytes })
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: senderKeypair.publicKey,
payer: senderKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.signers([senderKeypair])
.rpc();
// [6] Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction(
{
signature: mintTx,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed",
);
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
```
### 4.9. Full Solana transfer script (Self-managed)
The complete transfer script creates and signs a burn intent on Solana Devnet,
submits it to the Gateway API for attestation, and mints USDC on Solana Devnet
for the recipient. The script includes inline comments to explain what each
function does, making it easier to follow and modify if needed.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import * as crypto from "crypto";
import {
Wallet,
AnchorProvider,
setProvider,
Program,
utils,
} from "@coral-xyz/anchor";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
getAssociatedTokenAddressSync,
createAssociatedTokenAccountIdempotentInstruction,
} from "@solana/spl-token";
import {
u32be,
nu64be,
struct,
seq,
blob,
offset,
Layout,
} from "@solana/buffer-layout";
import bs58 from "bs58";
import {
RPC_ENDPOINT,
GATEWAY_WALLET_ADDRESS,
GATEWAY_MINTER_ADDRESS,
USDC_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
createKeypairFromEnv,
gatewayMinterIdl,
} from "./config.js";
const TRANSFER_AMOUNT = 1; // 1 USDC
const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
/* Type definitions */
// Custom layout for Solana PublicKey (32 bytes)
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
const pubkeyBuffer = src.toBuffer();
pubkeyBuffer.copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
// Custom layout for 256-bit unsigned integers
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
const buffer = b.subarray(offset, offset + 32);
return buffer.readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
// Type 'as any' used due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
/* Helpers */
// Construct burn intent for a given source
function createBurnIntent(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(USDC_ADDRESS),
destinationToken: addressToBytes32(USDC_ADDRESS),
sourceDepositor: addressToBytes32(sourceDepositor),
destinationRecipient: addressToBytes32(destinationRecipient),
sourceSigner: addressToBytes32(sourceSigner),
destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
},
};
}
// Encode burn intent as binary layout for signing
function encodeBurnIntent(bi: any): Buffer {
const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: bi.maxBlockHeight,
maxFee: bi.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: bi.spec.version,
sourceDomain: bi.spec.sourceDomain,
destinationDomain: bi.spec.destinationDomain,
sourceContract: hexToPublicKey(bi.spec.sourceContract),
destinationContract: hexToPublicKey(bi.spec.destinationContract),
sourceToken: hexToPublicKey(bi.spec.sourceToken),
destinationToken: hexToPublicKey(bi.spec.destinationToken),
sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
value: bi.spec.value,
salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
// Sign burn intent with Ed25519 keypair
function signBurnIntent(keypair: Keypair, payload: any): string {
const encoded = encodeBurnIntent(payload);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
// Convert Solana address to 32-byte hex string
function addressToBytes32(address: string): string {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Convert hex string to Solana PublicKey
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
// Decode attestation set from Gateway API response
function decodeAttestationSet(attestation: string) {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as {
attestations: Array<{
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
}>;
};
}
// Find PDA for token custody account
function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
// Find PDA for transfer spec hash tracking
function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
/* Main logic */
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR || !process.env.RECIPIENT_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR and RECIPIENT_KEYPAIR must be set");
}
const senderKeypair = createKeypairFromEnv(
process.env.SOLANA_PRIVATE_KEYPAIR,
);
const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
console.log(`Using account: ${senderKeypair.publicKey.toBase58()}`);
console.log(`Transferring balances from: Solana Devnet`);
// [1] Create recipient's Associated Token Account
const recipientAta = getAssociatedTokenAddressSync(
usdcMint,
recipientKeypair.publicKey,
);
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
senderKeypair.publicKey,
recipientAta,
recipientKeypair.publicKey,
usdcMint,
);
const tx = new Transaction().add(createAtaIx);
await sendAndConfirmTransaction(connection, tx, [senderKeypair]);
// [2] Create and sign burn intent
console.log(`Creating burn intent from Solana Devnet → Solana Devnet...`);
const burnIntent = createBurnIntent({
sourceDepositor: senderKeypair.publicKey.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: senderKeypair.publicKey.toBase58(),
});
const burnIntentSignature = signBurnIntent(senderKeypair, burnIntent);
const request = [{ burnIntent, signature: burnIntentSignature }];
console.log("Signed burn intent.");
// [3] Request attestation from Gateway API
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
const json = await response.json();
if (json.success === false) {
throw new Error(`Gateway API error: ${json.message}`);
}
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
const decoded = decodeAttestationSet(attestation);
// [4] Set up the minter client
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const anchorWallet = new Wallet(senderKeypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
// [5] Mint on Solana
const remainingAccounts = decoded.attestations.flatMap((e) => [
{
pubkey: findCustodyPda(e.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{ pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
{
pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
isWritable: true,
isSigner: false,
},
]);
const attestationBytes = Buffer.from(attestation.slice(2), "hex");
const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");
console.log("Minting funds on Solana Devnet...");
const mintTx = await minterProgram.methods
.gatewayMint({ attestation: attestationBytes, signature: signatureBytes })
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: senderKeypair.publicKey,
payer: senderKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.signers([senderKeypair])
.rpc();
// [6] Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction(
{
signature: mintTx,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed",
);
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
}
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.10. Run the Solana direct-mint script
Run the script to burn from your Solana Devnet Gateway balance and mint to the
recipient ATA on Solana Devnet.
Confirm these values before running:
* `SOLANA_PRIVATE_KEYPAIR` controls the source Solana Devnet wallet
* `RECIPIENT_KEYPAIR` controls the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* the source Solana wallet can submit the ATA creation and mint transactions on
Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the minted amount and Solana Devnet
mint transaction hash.
### 4.1. Create the Solana forwarding script
You can skip to the
[full transfer script](#4-7-full-solana-forwarding-script-self-managed) if you
prefer.
```shell theme={null}
touch transfer-from-sol.ts
```
### 4.2. Define constants and response types
This forwarding flow signs the burn intent on Solana Devnet, submits it with
forwarding enabled, and lets Circle complete the Solana Devnet mint.
```ts transfer-from-sol.ts expandable theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
type TransferSpec = ReturnType;
type BurnIntent = {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
};
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Add Solana burn intent helpers
The forwarding flow first estimates the burn-intent limits for the Solana Devnet
to Solana Devnet route, then signs the estimate-provided burn intent with the
source Solana keypair.
```ts transfer-from-sol.ts expandable theme={null}
// Construct transfer spec for Solana to Solana forwarding transfer
function createTransferSpec(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(USDC_ADDRESS),
destinationToken: addressToBytes32(USDC_ADDRESS),
sourceDepositor: addressToBytes32(sourceDepositor),
destinationRecipient: addressToBytes32(destinationRecipient),
sourceSigner: addressToBytes32(sourceSigner),
destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
};
}
// Construct burn intent with estimate-provided limits
function createBurnIntent(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
}): BurnIntent {
return params;
}
```
### 4.4. Estimate forwarding fees
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
Derive the recipient ATA, build the transfer spec, then call the Gateway
estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
and `maxBlockHeight` values to include in the signed burn intent.
```ts transfer-from-sol.ts theme={null}
const senderKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientKeypair.publicKey,
);
const spec = createTransferSpec({
sourceDepositor: senderKeypair.publicKey.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: senderKeypair.publicKey.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
```
### 4.5. Sign and submit the forwarding burn intent
Create the burn intent with the estimate-provided limits, sign it with the
source Solana keypair, then submit it to the Gateway transfer endpoint with
`enableForwarder=true`.
```ts transfer-from-sol.ts theme={null}
const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
const signature = signBurnIntent(senderKeypair, burnIntent);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent, signature }]),
},
);
```
### 4.6. Poll for forwarding completion
The Forwarding Service completes the Solana Devnet mint for you. Poll the
transfer status endpoint until the transfer is confirmed, finalized, failed, or
expired.
```ts transfer-from-sol.ts theme={null}
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
```
### 4.7. Full Solana forwarding script (Self-managed)
The complete script estimates forwarding fees, signs the burn intent on Solana
Devnet, submits it with forwarding enabled, and polls until Circle completes the
Solana Devnet mint.
```ts transfer-from-sol.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import * as crypto from "crypto";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey, Keypair } from "@solana/web3.js";
import { blob, Layout, offset, struct, u32be } from "@solana/buffer-layout";
import bs58 from "bs58";
import { formatUnits } from "viem";
import {
GATEWAY_MINTER_ADDRESS,
GATEWAY_WALLET_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
USDC_ADDRESS,
createKeypairFromEnv,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
type TransferSpec = ReturnType;
type BurnIntent = {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
};
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const TRANSFER_SPEC_MAGIC = 0xca85def7;
const BURN_INTENT_MAGIC = 0x070afbc2;
/* Type definitions */
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
src.toBuffer().copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
class UInt256BE extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0) {
return b.subarray(offset, offset + 32).readBigUInt64BE(24);
}
encode(src: bigint, b: Buffer, offset = 0) {
const buffer = Buffer.alloc(32);
buffer.writeBigUInt64BE(BigInt(src), 24);
buffer.copy(b, offset);
return 32;
}
}
const uint256be = (property: string) => new UInt256BE(property);
const BurnIntentLayout = struct([
u32be("magic"),
uint256be("maxBlockHeight"),
uint256be("maxFee"),
u32be("transferSpecLength"),
struct(
[
u32be("magic"),
u32be("version"),
u32be("sourceDomain"),
u32be("destinationDomain"),
publicKey("sourceContract"),
publicKey("destinationContract"),
publicKey("sourceToken"),
publicKey("destinationToken"),
publicKey("sourceDepositor"),
publicKey("destinationRecipient"),
publicKey("sourceSigner"),
publicKey("destinationCaller"),
uint256be("value"),
blob(32, "salt"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any,
"spec",
),
] as any);
/* Helpers */
// Construct transfer spec for Solana to Solana forwarding transfer
function createTransferSpec(params: {
sourceDepositor: string;
destinationRecipient: string;
sourceSigner: string;
}) {
const { sourceDepositor, destinationRecipient, sourceSigner } = params;
return {
version: 1,
sourceDomain: SOLANA_DOMAIN,
destinationDomain: SOLANA_DOMAIN,
sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: addressToBytes32(USDC_ADDRESS),
destinationToken: addressToBytes32(USDC_ADDRESS),
sourceDepositor: addressToBytes32(sourceDepositor),
destinationRecipient: addressToBytes32(destinationRecipient),
sourceSigner: addressToBytes32(sourceSigner),
destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}`,
hookData: "0x",
};
}
// Construct burn intent with estimate-provided limits
function createBurnIntent(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: TransferSpec;
}): BurnIntent {
return params;
}
// Encode burn intent as binary layout for signing
function encodeBurnIntent(burnIntent: BurnIntent): Buffer {
const hookData = Buffer.from(burnIntent.spec.hookData.slice(2), "hex");
const prepared = {
magic: BURN_INTENT_MAGIC,
maxBlockHeight: burnIntent.maxBlockHeight,
maxFee: burnIntent.maxFee,
transferSpecLength: 340 + hookData.length,
spec: {
magic: TRANSFER_SPEC_MAGIC,
version: burnIntent.spec.version,
sourceDomain: burnIntent.spec.sourceDomain,
destinationDomain: burnIntent.spec.destinationDomain,
sourceContract: hexToPublicKey(burnIntent.spec.sourceContract),
destinationContract: hexToPublicKey(burnIntent.spec.destinationContract),
sourceToken: hexToPublicKey(burnIntent.spec.sourceToken),
destinationToken: hexToPublicKey(burnIntent.spec.destinationToken),
sourceDepositor: hexToPublicKey(burnIntent.spec.sourceDepositor),
destinationRecipient: hexToPublicKey(
burnIntent.spec.destinationRecipient,
),
sourceSigner: hexToPublicKey(burnIntent.spec.sourceSigner),
destinationCaller: hexToPublicKey(burnIntent.spec.destinationCaller),
value: burnIntent.spec.value,
salt: Buffer.from(burnIntent.spec.salt.slice(2), "hex"),
hookDataLength: hookData.length,
hookData,
},
};
const buffer = Buffer.alloc(72 + 340 + hookData.length);
const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
return buffer.subarray(0, bytesWritten);
}
// Sign burn intent with Ed25519 keypair
function signBurnIntent(keypair: Keypair, burnIntent: BurnIntent): string {
const encoded = encodeBurnIntent(burnIntent);
const prefixed = Buffer.concat([
Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
encoded,
]);
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([
Buffer.from("302e020100300506032b657004220420", "hex"),
Buffer.from(keypair.secretKey.slice(0, 32)),
]),
format: "der",
type: "pkcs8",
});
return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
}
// Convert Solana address to 32-byte hex string
function addressToBytes32(address: string): string {
return `0x${Buffer.from(bs58.decode(address)).toString("hex")}`;
}
// Convert hex string to Solana PublicKey
function hexToPublicKey(hex: string): PublicKey {
return new PublicKey(Buffer.from(hex.slice(2), "hex"));
}
// Serialize API payloads with bigint values
function stringifyBigInts(obj: T): string {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
/* Main logic */
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR || !process.env.RECIPIENT_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR and RECIPIENT_KEYPAIR must be set");
}
const senderKeypair = createKeypairFromEnv(
process.env.SOLANA_PRIVATE_KEYPAIR,
);
const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
recipientKeypair.publicKey,
);
console.log(`Sender (Solana Devnet): ${senderKeypair.publicKey.toBase58()}`);
console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log("Forwarding: enabled\n");
// [1] Create transfer spec and estimate forwarding fees
const spec = createTransferSpec({
sourceDepositor: senderKeypair.publicKey.toBase58(),
destinationRecipient: recipientAta.toBase58(),
sourceSigner: senderKeypair.publicKey.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
if (!estimateResponse.ok) {
console.error("Estimate API error status:", estimateResponse.status);
console.error(await estimateResponse.text());
throw new Error("Estimate API request failed");
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
if (!estimatedBurnIntent) {
throw new Error("Estimate API response missing burnIntent");
}
const maxFee = BigInt(estimatedBurnIntent.maxFee);
const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
if (estimateResult.fees.forwardingFee) {
console.log(
`Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
);
}
console.log(
`Estimated maxFee: ${formatUnits(maxFee, 6)} ${estimateResult.fees.token}`,
);
// [2] Create, sign, and submit burn intent
const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
const signature = signBurnIntent(senderKeypair, burnIntent);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent, signature }]),
},
);
if (!transferResponse.ok) {
console.error("Gateway API error status:", transferResponse.status);
console.error(await transferResponse.text());
throw new Error("Gateway API request failed");
}
const transferResult = (await transferResponse.json()) as TransferResponse;
if (!transferResult.transferId) {
throw new Error("Gateway API response missing transferId");
}
console.log(`Transfer ID: ${transferResult.transferId}`);
// [3] Poll for forwarding completion
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
if (!pollResponse.ok) {
console.error(`Poll error: ${pollResponse.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollResponse.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
console.log(
`Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
);
console.log(`Forwarded ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
return;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error("Polling timed out waiting for transfer completion");
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.8. Run the Solana forwarding script
Run the script to burn from your Solana Devnet Gateway balance and let the
Forwarding Service mint to the recipient ATA on Solana Devnet.
Confirm these values before running:
* `SOLANA_PRIVATE_KEYPAIR` for the source Solana Devnet wallet
* `RECIPIENT_KEYPAIR` for the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Solana Devnet
* the recipient ATA exists on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-sol
```
When the transfer succeeds, the script logs the transfer ID, status, Solana
Devnet mint transaction hash, and forwarded amount.
## Step 4. Transfer USDC from EVM to Solana
This step transfers USDC from your Arc Testnet Gateway balance to Solana Devnet.
Both paths create and submit the burn intent from Arc Testnet first. Direct mint
then retrieves the Gateway attestation and calls `gatewayMint(...)` on Solana
Devnet from your self-managed Solana wallet, while Forwarding Service lets
Circle complete the Solana mint for you.
### 4.1. Create the script file
You can skip to the
[full transfer script](#4-9-full-evm-to-solana-transfer-script-self-managed) if
you prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and types
The validated script uses Arc Testnet as the source chain and Solana Devnet as
the destination chain. For now, the transfer amount is set to `0.1` USDC.
```ts transfer-from-evm.ts expandable theme={null}
type MintAttestation = {
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
};
type MintAttestationSet = {
attestations: MintAttestation[];
};
/* Constants */
const SOURCE_CHAIN = {
chain: arcTestnet,
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
usdc: "0x3600000000000000000000000000000000000000" as Hex,
domain: 26,
} as const;
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const eip712Domain = { name: "GatewayWallet", version: "1" } as const;
```
### 4.3. Add EIP-712 and attestation layouts
EVM source transfers use EIP-712 typed data for the burn intent. The Solana
destination mint still needs to decode the Gateway attestation set so the script
can build the remaining accounts for `gatewayMint`.
```ts transfer-from-evm.ts expandable theme={null}
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
src.toBuffer().copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
```
### 4.4. Add helper functions
The burn intent uses EVM `bytes32` values for Arc Testnet and Solana `bytes32`
values for the destination Gateway Minter, USDC mint, recipient ATA, and zero
destination caller.
```ts transfer-from-evm.ts expandable theme={null}
// Construct burn intent for EVM to Solana transfer
function createBurnIntent(params: {
depositorAddress: Hex;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}` as Hex,
hookData: "0x" as Hex,
},
};
}
// Format burn intent as EIP-712 typed data for signing
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: burnIntent,
} as const;
}
// Decode attestation set from Gateway API response
function decodeAttestationSet(attestation: Hex): MintAttestationSet {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as MintAttestationSet;
}
// Find PDA for token custody account
function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
// Find PDA for transfer spec hash tracking
function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
// Pad EVM address to 32 bytes
function evmAddressToBytes32(address: Hex): Hex {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
// Convert Solana address to 32-byte hex string
function solanaAddressToBytes32(address: string): Hex {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Serialize typed data (convert bigints to strings)
function stringifyBigInts(obj: T): string {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
// Get EVM keypair from environment variable
function createEvmAccount() {
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error("EVM_PRIVATE_KEY must be set");
}
const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
? (process.env.EVM_PRIVATE_KEY as Hex)
: (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);
return privateKeyToAccount(privateKey);
}
```
### 4.5. Initialize wallets and create recipient ATA
Use `EVM_PRIVATE_KEY` as the Arc Testnet source signer and
`SOLANA_PRIVATE_KEYPAIR` as the Solana Devnet recipient and mint signer. The
recipient must receive USDC in an associated token account (ATA), not directly
in a wallet account.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
```ts transfer-from-evm.ts theme={null}
const evmAccount = createEvmAccount();
const solanaKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
const recipientAta = getAssociatedTokenAddressSync(
usdcMint,
solanaKeypair.publicKey,
);
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
solanaKeypair.publicKey,
recipientAta,
solanaKeypair.publicKey,
usdcMint,
);
const createAtaTx = new Transaction().add(createAtaIx);
await sendAndConfirmTransaction(connection, createAtaTx, [solanaKeypair]);
```
### 4.6. Create and sign burn intent
Create one burn intent for Arc Testnet and sign it with the self-managed EVM
wallet.
```ts transfer-from-evm.ts theme={null}
const burnIntent = createBurnIntent({
depositorAddress: evmAccount.address,
recipientAta: recipientAta.toBase58(),
});
const typedData = burnIntentTypedData(burnIntent);
const signature = await evmAccount.signTypedData(typedData);
```
### 4.7. Request attestation from Gateway API
```ts transfer-from-evm.ts theme={null}
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = (await response.json()) as {
attestation?: Hex;
signature?: Hex;
};
const { attestation, signature: mintSignature } = json;
if (!attestation || !mintSignature) {
throw new Error("Gateway API response missing attestation or signature");
}
```
### 4.8. Set up minter client and mint on Solana
After Gateway returns the attestation and operator signature, use them to call
`gatewayMint` on Solana Devnet.
The ordered remaining-account list and PDA derivations are documented in
[Solana Programs and
Interfaces](/gateway/references/solana-programs#gatewaymint). For static
instruction definitions, use the onchain IDLs linked from that page.
```ts transfer-from-evm.ts expandable theme={null}
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const anchorWallet = new Wallet(solanaKeypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
const decoded = decodeAttestationSet(attestation);
const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
{
pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{
pubkey: attestationItem.destinationRecipient,
isWritable: true,
isSigner: false,
},
{
pubkey: findTransferSpecHashPda(
attestationItem.transferSpecHash,
minterProgramId,
),
isWritable: true,
isSigner: false,
},
]);
const mintTx = await minterProgram.methods
.gatewayMint({
attestation: Buffer.from(attestation.slice(2), "hex"),
signature: Buffer.from(mintSignature.slice(2), "hex"),
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: solanaKeypair.publicKey,
payer: solanaKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.signers([solanaKeypair])
.rpc();
```
### 4.9. Full EVM to Solana transfer script (Self-managed)
The complete transfer script creates the Solana recipient ATA, signs an Arc
Testnet burn intent, submits it to Gateway for attestation, and mints USDC on
Solana Devnet.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import {
Wallet,
AnchorProvider,
Program,
setProvider,
utils,
} from "@coral-xyz/anchor";
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Connection,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
blob,
Layout,
nu64be,
offset,
seq,
struct,
u32be,
} from "@solana/buffer-layout";
import bs58 from "bs58";
import { pad, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
import {
GATEWAY_MINTER_ADDRESS,
RPC_ENDPOINT,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
USDC_ADDRESS,
createKeypairFromEnv,
gatewayMinterIdl,
} from "./config.js";
type MintAttestation = {
destinationToken: PublicKey;
destinationRecipient: PublicKey;
transferSpecHash: Uint8Array;
};
type MintAttestationSet = {
attestations: MintAttestation[];
};
/* Constants */
const SOURCE_CHAIN = {
chain: arcTestnet,
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
usdc: "0x3600000000000000000000000000000000000000" as Hex,
domain: 26,
} as const;
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT64 = 2n ** 64n - 1n;
const eip712Domain = { name: "GatewayWallet", version: "1" } as const;
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
/* Type definitions */
class PublicKeyLayout extends Layout {
constructor(property: string) {
super(32, property);
}
decode(b: Buffer, offset = 0): PublicKey {
return new PublicKey(b.subarray(offset, offset + 32));
}
encode(src: PublicKey, b: Buffer, offset = 0): number {
src.toBuffer().copy(b, offset);
return 32;
}
}
const publicKey = (property: string) => new PublicKeyLayout(property);
const MintAttestationElementLayout = struct([
publicKey("destinationToken"),
publicKey("destinationRecipient"),
nu64be("value"),
blob(32, "transferSpecHash"),
u32be("hookDataLength"),
blob(offset(u32be(), -4), "hookData"),
] as any);
const MintAttestationSetLayout = struct([
u32be("magic"),
u32be("version"),
u32be("destinationDomain"),
publicKey("destinationContract"),
publicKey("destinationCaller"),
nu64be("maxBlockHeight"),
u32be("numAttestations"),
seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
] as any);
/* Helpers */
// Construct burn intent for EVM to Solana transfer
function createBurnIntent(params: {
depositorAddress: Hex;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
maxBlockHeight: MAX_UINT64,
maxFee: MAX_FEE,
spec: {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}` as Hex,
hookData: "0x" as Hex,
},
};
}
// Format burn intent as EIP-712 typed data for signing
function burnIntentTypedData(burnIntent: ReturnType) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: burnIntent,
} as const;
}
// Decode attestation set from Gateway API response
function decodeAttestationSet(attestation: Hex): MintAttestationSet {
const buffer = Buffer.from(attestation.slice(2), "hex");
return MintAttestationSetLayout.decode(buffer) as MintAttestationSet;
}
// Find PDA for token custody account
function findCustodyPda(
mint: PublicKey,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("gateway_minter_custody"), mint.toBuffer()],
minterProgramId,
)[0];
}
// Find PDA for transfer spec hash tracking
function findTransferSpecHashPda(
transferSpecHash: Uint8Array | Buffer,
minterProgramId: PublicKey,
): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
minterProgramId,
)[0];
}
// Pad EVM address to 32 bytes
function evmAddressToBytes32(address: Hex): Hex {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
// Convert Solana address to 32-byte hex string
function solanaAddressToBytes32(address: string): Hex {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Serialize typed data (convert bigints to strings)
function stringifyBigInts(obj: T): string {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
// Get EVM keypair from environment variable
function createEvmAccount() {
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error("EVM_PRIVATE_KEY must be set");
}
const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
? (process.env.EVM_PRIVATE_KEY as Hex)
: (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);
return privateKeyToAccount(privateKey);
}
/* Main logic */
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR must be set");
}
const evmAccount = createEvmAccount();
const solanaKeypair = createKeypairFromEnv(
process.env.SOLANA_PRIVATE_KEYPAIR,
);
console.log(`Sender (${SOURCE_CHAIN.chain.name}): ${evmAccount.address}`);
console.log(
`Recipient (Solana Devnet): ${solanaKeypair.publicKey.toBase58()}`,
);
const connection = new Connection(RPC_ENDPOINT, "confirmed");
const usdcMint = new PublicKey(USDC_ADDRESS);
// [1] Create recipient's Associated Token Account
const recipientAta = getAssociatedTokenAddressSync(
usdcMint,
solanaKeypair.publicKey,
);
const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
solanaKeypair.publicKey,
recipientAta,
solanaKeypair.publicKey,
usdcMint,
);
const createAtaTx = new Transaction().add(createAtaIx);
await sendAndConfirmTransaction(connection, createAtaTx, [solanaKeypair]);
// [2] Create and sign burn intent
const burnIntent = createBurnIntent({
depositorAddress: evmAccount.address,
recipientAta: recipientAta.toBase58(),
});
const typedData = burnIntentTypedData(burnIntent);
const signature = await evmAccount.signTypedData(typedData);
// [3] Request attestation from Gateway API
const response = await fetch(
"https://gateway-api-testnet.circle.com/v1/transfer",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
if (!response.ok) {
console.error("Gateway API error status:", response.status);
console.error(await response.text());
throw new Error("Gateway API request failed");
}
const json = (await response.json()) as {
attestation?: Hex;
signature?: Hex;
};
console.log("Gateway API response:", JSON.stringify(json, null, 2));
const { attestation, signature: mintSignature } = json;
if (!attestation || !mintSignature) {
throw new Error("Gateway API response missing attestation or signature");
}
// [4] Set up the minter client
const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
const anchorWallet = new Wallet(solanaKeypair);
const provider = new AnchorProvider(
connection,
anchorWallet,
AnchorProvider.defaultOptions(),
);
setProvider(provider);
const minterProgram = new Program(gatewayMinterIdl, provider);
const [minterPda] = PublicKey.findProgramAddressSync(
[Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
minterProgramId,
);
const decoded = decodeAttestationSet(attestation);
const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
{
pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
isWritable: true,
isSigner: false,
},
{
pubkey: attestationItem.destinationRecipient,
isWritable: true,
isSigner: false,
},
{
pubkey: findTransferSpecHashPda(
attestationItem.transferSpecHash,
minterProgramId,
),
isWritable: true,
isSigner: false,
},
]);
// [5] Mint on Solana
console.log("Minting funds on Solana Devnet...");
const mintTx = await minterProgram.methods
.gatewayMint({
attestation: Buffer.from(attestation.slice(2), "hex"),
signature: Buffer.from(mintSignature.slice(2), "hex"),
})
.accountsPartial({
gatewayMinter: minterPda,
destinationCaller: solanaKeypair.publicKey,
payer: solanaKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.remainingAccounts(remainingAccounts)
.signers([solanaKeypair])
.rpc();
// [6] Wait for confirmation
const latest = await connection.getLatestBlockhash();
await connection.confirmTransaction(
{
signature: mintTx,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed",
);
console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.10. Run the EVM to Solana direct-mint script
Run the script to burn from your Arc Testnet Gateway balance and mint to the
recipient ATA on Solana Devnet.
Confirm these values before running:
* `EVM_PRIVATE_KEY` controls the source Arc Testnet wallet
* `SOLANA_PRIVATE_KEYPAIR` controls the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Arc Testnet
* the destination Solana wallet can submit the ATA creation and mint
transactions on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the minted amount and Solana Devnet
mint transaction hash.
### 4.1. Create the EVM to Solana forwarding script
You can skip to the
[full transfer script](#4-8-full-evm-to-solana-forwarding-script-self-managed)
if you prefer.
```shell theme={null}
touch transfer-from-evm.ts
```
### 4.2. Define constants and response types
This forwarding flow signs the burn intent on Arc Testnet, submits it with
forwarding enabled, and lets Circle complete the Solana Devnet mint.
```ts transfer-from-evm.ts expandable theme={null}
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
```
### 4.3. Add EIP-712 helpers
The forwarding flow first estimates the burn-intent limits for the Arc Testnet
to Solana Devnet route, then signs the estimated burn intent with your
self-managed EVM wallet.
```ts transfer-from-evm.ts expandable theme={null}
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
```
### 4.4. Create the transfer spec helpers
The transfer spec uses EVM `bytes32` values for Arc Testnet and Solana `bytes32`
values for the destination Gateway Minter, USDC mint, and recipient ATA.
```ts transfer-from-evm.ts expandable theme={null}
// Construct transfer spec for EVM to Solana forwarding transfer
function createTransferSpec(params: {
depositorAddress: Hex;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}` as Hex,
hookData: "0x" as Hex,
};
}
// Format burn intent as EIP-712 typed data for signing
function burnIntentTypedData(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: ReturnType;
}) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: params,
} as const;
}
```
### 4.5. Estimate forwarding fees
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
Derive the recipient ATA, build the transfer spec, then call the Gateway
estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
and `maxBlockHeight` values to include in the signed burn intent.
```ts transfer-from-evm.ts theme={null}
const evmAccount = createEvmAccount();
const solanaKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
solanaKeypair.publicKey,
);
const spec = createTransferSpec({
depositorAddress: evmAccount.address,
recipientAta: recipientAta.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
```
### 4.6. Sign and submit the forwarding burn intent
Sign the estimated burn intent with the self-managed Arc Testnet wallet, then
submit it to the Gateway transfer endpoint with `enableForwarder=true`.
```ts transfer-from-evm.ts theme={null}
const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
const signature = await evmAccount.signTypedData(typedData);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
```
### 4.7. Poll for forwarding completion
The Forwarding Service completes the Solana Devnet mint for you. Poll the
transfer status endpoint until the transfer is confirmed, finalized, failed, or
expired.
```ts transfer-from-evm.ts theme={null}
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
```
### 4.8. Full EVM to Solana forwarding script (Self-managed)
The complete script estimates forwarding fees, signs the burn intent on Arc
Testnet, submits it with forwarding enabled, and polls until Circle completes
the Solana Devnet mint.
```ts transfer-from-evm.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import bs58 from "bs58";
import { formatUnits, pad, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
import {
GATEWAY_MINTER_ADDRESS,
SOLANA_DOMAIN,
SOLANA_ZERO_ADDRESS,
USDC_ADDRESS,
createKeypairFromEnv,
} from "./config.js";
type EstimateResponse = {
body: Array<{
burnIntent: {
maxFee: string;
maxBlockHeight: string;
};
}>;
fees: {
forwardingFee?: string;
token: string;
};
};
type TransferResponse = {
transferId?: string;
};
type TransferStatusResponse = {
status: string;
transactionHash?: string;
forwardingDetails?: {
failureReason?: string;
};
};
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const SOURCE_CHAIN = {
chain: arcTestnet,
gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
usdc: "0x3600000000000000000000000000000000000000" as Hex,
domain: 26,
} as const;
const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000;
const eip712Domain = { name: "GatewayWallet", version: "1" } as const;
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
/* Helpers */
// Construct transfer spec for EVM to Solana forwarding transfer
function createTransferSpec(params: {
depositorAddress: Hex;
recipientAta: string;
}) {
const { depositorAddress, recipientAta } = params;
return {
version: 1,
sourceDomain: SOURCE_CHAIN.domain,
destinationDomain: SOLANA_DOMAIN,
sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
sourceDepositor: evmAddressToBytes32(depositorAddress),
destinationRecipient: solanaAddressToBytes32(recipientAta),
sourceSigner: evmAddressToBytes32(depositorAddress),
destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
value: TRANSFER_VALUE,
salt: `0x${randomBytes(32).toString("hex")}` as Hex,
hookData: "0x" as Hex,
};
}
// Format burn intent as EIP-712 typed data for signing
function burnIntentTypedData(params: {
maxBlockHeight: bigint;
maxFee: bigint;
spec: ReturnType;
}) {
return {
types: { EIP712Domain, TransferSpec, BurnIntent },
domain: eip712Domain,
primaryType: "BurnIntent",
message: params,
} as const;
}
// Pad EVM address to 32 bytes
function evmAddressToBytes32(address: Hex): Hex {
return pad(address.toLowerCase() as Hex, { size: 32 });
}
// Convert Solana address to 32-byte hex string
function solanaAddressToBytes32(address: string): Hex {
const decoded = Buffer.from(bs58.decode(address));
return `0x${decoded.toString("hex")}`;
}
// Serialize typed data and convert bigints to strings
function stringifyBigInts(obj: T): string {
return JSON.stringify(obj, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
);
}
// Get EVM keypair from environment variable
function createEvmAccount() {
if (!process.env.EVM_PRIVATE_KEY) {
throw new Error("EVM_PRIVATE_KEY must be set");
}
const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
? (process.env.EVM_PRIVATE_KEY as Hex)
: (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);
return privateKeyToAccount(privateKey);
}
/* Main logic */
async function main() {
if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
throw new Error("SOLANA_PRIVATE_KEYPAIR must be set");
}
const evmAccount = createEvmAccount();
const solanaKeypair = createKeypairFromEnv(
process.env.SOLANA_PRIVATE_KEYPAIR,
);
const recipientAta = getAssociatedTokenAddressSync(
new PublicKey(USDC_ADDRESS),
solanaKeypair.publicKey,
);
console.log(`Sender (${SOURCE_CHAIN.chain.name}): ${evmAccount.address}`);
console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log("Forwarding: enabled\n");
// [1] Create transfer spec and estimate forwarding fees
const spec = createTransferSpec({
depositorAddress: evmAccount.address,
recipientAta: recipientAta.toBase58(),
});
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ spec }]),
},
);
if (!estimateResponse.ok) {
console.error("Estimate API error status:", estimateResponse.status);
console.error(await estimateResponse.text());
throw new Error("Estimate API request failed");
}
const estimateResult = (await estimateResponse.json()) as EstimateResponse;
const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
if (!estimatedBurnIntent) {
throw new Error("Estimate API response missing burnIntent");
}
const maxFee = BigInt(estimatedBurnIntent.maxFee);
const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
if (estimateResult.fees.forwardingFee) {
console.log(
`Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
);
}
console.log(
`Estimated maxFee: ${formatUnits(maxFee, 6)} ${estimateResult.fees.token}`,
);
// [2] Create, sign, and submit burn intent
const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
const signature = await evmAccount.signTypedData(typedData);
const transferResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
},
);
if (!transferResponse.ok) {
console.error("Gateway API error status:", transferResponse.status);
console.error(await transferResponse.text());
throw new Error("Gateway API request failed");
}
const transferResult = (await transferResponse.json()) as TransferResponse;
if (!transferResult.transferId) {
throw new Error("Gateway API response missing transferId");
}
console.log(`Transfer ID: ${transferResult.transferId}`);
// [3] Poll for forwarding completion
const pollStart = Date.now();
while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
const pollResponse = await fetch(
`${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
);
if (!pollResponse.ok) {
console.error(`Poll error: ${pollResponse.status}`);
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
continue;
}
const details = (await pollResponse.json()) as TransferStatusResponse;
console.log(`Status: ${details.status}`);
if (details.status === "finalized" || details.status === "confirmed") {
console.log(
`Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
);
console.log(`Forwarded ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
return;
}
if (details.status === "failed") {
const reason = details.forwardingDetails?.failureReason ?? "unknown";
throw new Error(`Transfer failed: ${reason}`);
}
if (details.status === "expired") {
throw new Error("Transfer attestation expired before forwarding");
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error("Polling timed out waiting for transfer completion");
}
/* Main invocation */
main().catch((error) => {
console.error("\nError:", error);
process.exit(1);
});
```
### 4.9. Run the EVM to Solana forwarding script
Run the script to burn from your Arc Testnet Gateway balance and let the
Forwarding Service mint to the recipient ATA on Solana Devnet.
Confirm these values before running:
* `EVM_PRIVATE_KEY` for the source Arc Testnet wallet
* `SOLANA_PRIVATE_KEYPAIR` for the destination Solana Devnet wallet
* the source depositor has a Gateway balance on Arc Testnet
* the recipient ATA exists on Solana Devnet
Gateway fees are charged per burn intent and are based on the source
blockchain you burn from. Choosing where to hold and burn Gateway balances can
affect transfer costs. For fee details, see [Gateway
Fees](/gateway/references/fees).
```shell theme={null}
npm run transfer-from-evm
```
When the transfer succeeds, the script logs the transfer ID, status, Solana
Devnet mint transaction hash, and forwarded amount.
# Gateway smart contract addresses on EVM
Source: https://developers.circle.com/gateway/references/contract-addresses
Circle Gateway smart contract addresses on EVM
The following sections list the onchain addresses for the
[Gateway EVM smart contracts](/gateway/references/contract-interfaces-and-events).
## Testnet contract addresses
### GatewayWallet
| Chain | Domain | Address |
| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Arc Testnet | 26 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://testnet.arcscan.app/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Arbitrum Sepolia | 3 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia.arbiscan.io/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Avalanche Fuji | 1 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://testnet.snowscan.xyz/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Base Sepolia | 6 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia.basescan.org/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Ethereum Sepolia | 0 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia.etherscan.io/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| HyperEVM Testnet | 19 | `0x0077777d7EBA4688BDeF3E311b846F25870A19B9` |
| OP Sepolia | 2 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia-optimism.etherscan.io/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Polygon PoS Amoy | 7 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://amoy.polygonscan.com/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Sei Atlantic | 16 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://testnet.seiscan.io/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Solana Devnet | 5 | [`GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu`](https://solscan.io/account/GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu?cluster=devnet) |
| Sonic Testnet | 13 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://testnet.sonicscan.org/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| Unichain Sepolia | 10 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia.uniscan.xyz/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
| World Chain Sepolia | 14 | [`0x0077777d7EBA4688BDeF3E311b846F25870A19B9`](https://sepolia.worldscan.org/address/0x0077777d7EBA4688BDeF3E311b846F25870A19B9) |
### GatewayMinter
| Chain | Domain | Address |
| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Arc Testnet | 26 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://testnet.arcscan.app/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Arbitrum Sepolia | 3 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia.arbiscan.io/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Avalanche Fuji | 1 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://testnet.snowscan.xyz/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Base Sepolia | 6 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia.basescan.org/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Ethereum Sepolia | 0 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia.etherscan.io/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| HyperEVM Testnet | 19 | `0x0022222ABE238Cc2C7Bb1f21003F0a260052475B` |
| OP Sepolia | 2 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia-optimism.etherscan.io/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Polygon PoS Amoy | 7 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://amoy.polygonscan.com/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Sei Atlantic | 16 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://testnet.seiscan.io/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Solana Devnet | 5 | [`GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr`](https://solscan.io/account/GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr?cluster=devnet) |
| Sonic Testnet | 13 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://testnet.sonicscan.org/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| Unichain Sepolia | 10 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia.uniscan.xyz/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
| World Chain Sepolia | 14 | [`0x0022222ABE238Cc2C7Bb1f21003F0a260052475B`](https://sepolia.worldscan.org/address/0x0022222ABE238Cc2C7Bb1f21003F0a260052475B) |
## Mainnet contract addresses
### GatewayWallet
| Chain | Domain | Address |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Arbitrum | 3 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://arbiscan.io/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Avalanche | 1 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://snowscan.xyz/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Base | 6 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://basescan.org/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Ethereum | 0 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://etherscan.io/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| HyperEVM | 19 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://hyperevmscan.io/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| OP | 2 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://optimistic.etherscan.io/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Polygon PoS | 7 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://polygonscan.com/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Sei | 16 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://seiscan.io/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Sonic | 13 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://sonicscan.org/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| Unichain | 10 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://uniscan.xyz/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
| World Chain | 14 | [`0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE`](https://worldscan.org/address/0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE) |
### GatewayMinter
| Chain | Domain | Address |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| Arbitrum | 3 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://arbiscan.io/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Avalanche | 1 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://snowscan.xyz/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Base | 6 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://basescan.org/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Ethereum | 0 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://etherscan.io/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| HyperEVM | 19 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://hyperevmscan.io/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| OP | 2 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://optimistic.etherscan.io/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Polygon PoS | 7 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://polygonscan.com/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Sei | 16 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://seiscan.io/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Sonic | 13 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://sonicscan.org/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| Unichain | 10 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://uniscan.xyz/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
| World Chain | 14 | [`0x2222222d7164433c4C09B0b0D809a9b52C04C205`](https://worldscan.org/address/0x2222222d7164433c4C09B0b0D809a9b52C04C205) |
# Gateway contract interfaces and events on EVM
Source: https://developers.circle.com/gateway/references/contract-interfaces-and-events
Learn about the Circle Gateway smart contracts on EVM
The [wallet and minter smart contracts](/gateway/references/contract-addresses)
have a set of interfaces and events that you can use to build with Circle
Gateway. This page describes the user-facing methods of these contracts.
## Interfaces
This section provides information on the public methods of the `GatewayWallet`
and `GatewayMinter` smart contracts. The full ABIs are
[available on GitHub](https://github.com/circlefin/evm-gateway-contracts).
### GatewayWallet
#### deposit
Deposit tokens after approving this contract for the token. The resulting
balance belongs to the function caller.
```solidity Solidity theme={null}
function deposit(address token, uint256 value) external;
```
**Parameters**
| Name | Type | Description |
| ------- | --------- | ----------------------------------- |
| `token` | `address` | The address of the token to deposit |
| `value` | `uint256` | The amount of tokens to deposit |
#### depositFor
Deposit tokens on behalf of another address after approving this contract for
the token. The resulting balance belongs to the address specified by the
`depositor` parameter, not the function caller.
```solidity Solidity theme={null}
function depositFor(address token, address depositor, uint256 value) external;
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ------------------------------------------------- |
| `token` | `address` | The address of the token to deposit |
| `depositor` | `address` | The address that should own the resulting balance |
| `value` | `uint256` | The amount of tokens to deposit |
#### depositWithPermit
Deposit tokens with an EIP-2612 permit. The resulting balance belongs to the
`owner` specified in the permit. The permit's `spender` must be the address of
the GatewayWallet contract. The full permitted `value` is always deposited.
```solidity Solidity theme={null}
function depositWithPermit(address token, address owner, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
```
**Parameters**
| Name | Type | Description |
| ---------- | --------- | ------------------------------------------------------------------------------------------------- |
| `token` | `address` | The address of the token to deposit |
| `owner` | `address` | The depositor's address |
| `value` | `uint256` | The amount of tokens to deposit |
| `deadline` | `uint256` | The unix timestamp at which the signature expires, or max `uint256` value to signal no expiration |
| `v` | `uint8` | `v` of the signature |
| `r` | `bytes32` | `r` of the signature |
| `s` | `bytes32` | `s` of the signature |
#### depositWithPermit
Deposit tokens with an EIP-2612 permit (using the EIP-7597 extension), passing
the signature as bytes to allow for SCA deposits. The resulting balance belongs
to the `owner` specified in the permit. The permit's `spender` must be the
address of the GatewayWallet contract. The full permitted `value` is always
deposited. EOA wallet signatures should be packed in the order of r, s, v.
```solidity Solidity theme={null}
function depositWithPermit(address token, address owner, uint256 value, uint256 deadline, bytes calldata signature) external;
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | -------------------------------------------------------------------------------------------- |
| `token` | `address` | The address of the token to deposit |
| `owner` | `address` | The depositor's address |
| `value` | `uint256` | The amount of tokens to deposit |
| `deadline` | `uint256` | The unix time at which the signature expires, or max `uint256` value to signal no expiration |
| `signature` | `bytes` | Signature bytes signed by an EOA wallet or a contract wallet |
#### depositWithAuthorization
Deposit tokens with an ERC-3009 authorization. The resulting balance in this
contract belongs to the `from` specified in the authorization. The
authorization's `to` must be the address of the GatewayWallet contract.
```solidity Solidity theme={null}
function depositWithAuthorization(address token, address from, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external;
```
**Parameters**
| Name | Type | Description |
| ------------- | --------- | ---------------------------------------------------------- |
| `token` | `address` | The address of the token to deposit |
| `from` | `address` | The depositor's address |
| `value` | `uint256` | The amount of tokens to deposit |
| `validAfter` | `uint256` | The unix timestamp after which the authorization is valid |
| `validBefore` | `uint256` | The unix timestamp before which the authorization is valid |
| `nonce` | `bytes32` | Unique nonce |
| `v` | `uint8` | v of the signature |
| `r` | `bytes32` | r of the signature |
| `s` | `bytes32` | s of the signature |
#### depositWithAuthorization
Deposit tokens with an ERC-3009 authorization (using the ERC-7598 extension),
passing the signature as bytes to allow for SCA deposits. The resulting balance
in this contract belongs to the `from` specified in the authorization. The
authorization's `to` must be the address of this contract. The transfer will be
done via `receiveWithAuthorization`. EOA wallet signatures should be packed in
the order of r, s, v.
```solidity Solidity theme={null}
function depositWithAuthorization(address token, address from, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes calldata signature) external;
```
**Parameters**
| Name | Type | Description |
| ------------- | --------- | ------------------------------------------------------------ |
| `token` | `address` | The address of the token to deposit |
| `from` | `address` | The depositor's address |
| `value` | `uint256` | The amount of tokens to deposit |
| `validAfter` | `uint256` | The unix timestamp after which the authorization is valid |
| `validBefore` | `uint256` | The unix timestamp before which the authorization is valid |
| `nonce` | `bytes32` | Unique nonce |
| `signature` | `bytes` | Signature bytes signed by an EOA wallet or a contract wallet |
#### totalBalance
Returns the total balance of a depositor for a given token. This will always be
equal to the sum of `availableBalance` and `withdrawingBalance`.
```solidity Solidity theme={null}
function totalBalance(address token, address depositor) public view returns (uint256);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
| `depositor` | `address` | The depositor to check |
**Returns**
| Name | Type | Description |
| -------- | --------- | ------------------------------------------------ |
| `` | `uint256` | The total balance of the depositor for the token |
#### availableBalance
Returns the balance that is available to the depositor, subject to deposits
being observed by Circle in a finalized block and excepting in-flight transfers.
```solidity Solidity theme={null}
function availableBalance(address token, address depositor) public view returns (uint256);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
| `depositor` | `address` | The depositor to check |
**Returns**
| Name | Type | Description |
| -------- | --------- | ---------------------------------------------------- |
| `` | `uint256` | The available balance of the depositor for the token |
#### withdrawingBalance
Returns the balance that is in the process of being withdrawn from the
GatewayWallet contract.
```solidity Solidity theme={null}
function withdrawingBalance(address token, address depositor) public view returns (uint256);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
| `depositor` | `address` | The depositor to check |
**Returns**
| Name | Type | Description |
| -------- | --------- | ------------------------------------------------------ |
| `` | `uint256` | The withdrawing balance of the depositor for the token |
#### withdrawableBalance
Returns the balance that is withdrawable as of the current block. This will
either be 0 or `withdrawingBalance`.
```solidity Solidity theme={null}
function withdrawableBalance(address token, address depositor) public view returns (uint256);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
| `depositor` | `address` | The depositor to check |
**Returns**
| Name | Type | Description |
| -------- | --------- | ------------------------------------------------------- |
| `` | `uint256` | The withdrawable balance of the depositor for the token |
#### balanceOf
Returns the balance of a depositor for a given token and balance type,
compatible with ERC-1155. The "token" `id` is encoded as
`uint256(bytes32(abi.encodePacked(uint96(BALANCE_TYPE), address(token))))`,
where `BALANCE_TYPE` is 0 for `Total`, 1 for `Available`, 2 for `Withdrawing`,
and 3 for `Withdrawable`.
```solidity Solidity theme={null}
function balanceOf(address depositor, uint256 id) public view returns (uint256 balance);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `depositor` | `address` | The depositor to check |
| `id` | `uint256` | The packed token and balance type |
**Returns**
| Name | Type | Description |
| --------- | --------- | ----------------------------------------------------------- |
| `balance` | `uint256` | The balance of the depositor for the token and balance type |
#### balanceOfBatch
The batch version of `balanceOf`, compatible with ERC-1155. `depositors` and
`ids` must be the same length. See the documentation for `balanceOf` for the
format of `ids`.
```solidity Solidity theme={null}
function balanceOfBatch(address[] calldata depositors, uint256[] calldata ids) external view override returns (uint256[] memory balances);
```
**Parameters**
| Name | Type | Description |
| ------------ | ----------- | ----------------------------------- |
| `depositors` | `address[]` | The depositors to check |
| `ids` | `uint256[]` | The packed tokens and balance types |
**Returns**
| Name | Type | Description |
| ---------- | ----------- | --------------------------------------------------------------- |
| `balances` | `uint256[]` | The balances of the depositors for the tokens and balance types |
#### addDelegate
Allow a delegate to transfer the caller's balance of the specified token. This
acts as a full allowance for `delegate` on the `token` balance of the function
caller.
```solidity Solidity theme={null}
function addDelegate(address token, address delegate) external;
```
**Parameters**
| Name | Type | Description |
| ---------- | --------- | ----------------------------------------------------------------- |
| `token` | `address` | The address of the token that `delegate` should be authorized for |
| `delegate` | `address` | The address being authorized |
#### removeDelegate
Stop allowing a delegate to transfer the caller's balance of the specified
token. This revocation is not respected for burn intents that have already been
signed, so that burns cannot be prevented by removing the delegate.
```solidity Solidity theme={null}
function removeDelegate(address token, address delegate) external;
```
**Parameters**
| Name | Type | Description |
| ---------- | --------- | ------------------------------------------------------------------------ |
| `token` | `address` | The address of the token the delegate should no longer be authorized for |
| `delegate` | `address` | The address that should no longer be authorized |
#### isAuthorizedForBalance
Returns whether an address is authorized to transfer tokens on behalf of a
depositor.
```solidity Solidity theme={null}
function isAuthorizedForBalance(address token, address depositor, address addr) public view returns (bool);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
| `depositor` | `address` | The depositor to check |
| `addr` | `address` | The address to check |
**Returns**
| Name | Type | Description |
| -------- | ------ | ------------------------------------------------------ |
| `` | `bool` | `true` if the address is authorized, `false` otherwise |
#### withdrawalDelay
Returns the number of blocks that must pass after calling `initiateWithdrawal`
before a withdrawal can be completed.
```solidity Solidity theme={null}
function withdrawalDelay() public view returns (uint256);
```
**Returns**
| Name | Type | Description |
| -------- | --------- | ----------------------------------- |
| `` | `uint256` | The number of blocks that must pass |
#### withdrawalBlock
Returns the block height at which an in-progress withdrawal is withdrawable, or
0 if there is no in-progress withdrawal.
```solidity Solidity theme={null}
function withdrawalBlock(address token, address depositor) public view returns (uint256);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ---------------------------------------------------------- |
| `token` | `address` | The address of the token of the in-progress withdrawal |
| `depositor` | `address` | The address of the depositor of the in-progress withdrawal |
**Returns**
| Name | Type | Description |
| -------- | --------- | ------------------------------------------------------------- |
| `` | `uint256` | The block number at which the withdrawal will be withdrawable |
#### initiateWithdrawal
Starts the withdrawal process. After `withdrawalDelay` blocks, `withdraw` may be
called to complete the withdrawal. Once a withdrawal has been initiated, that
amount can no longer be used. Repeated calls will add to the amount and reset
the timer.
```solidity Solidity theme={null}
function initiateWithdrawal(address token, uint256 value) external;
```
**Parameters**
| Name | Type | Description |
| ------- | --------- | ----------------------------------------------------- |
| `token` | `address` | The address of the token to initiate a withdrawal for |
| `value` | `uint256` | The amount to be withdrawn |
#### withdraw
Completes a withdrawal that was initiated at least `withdrawalDelay` blocks ago.
The funds are sent to the depositor (who must be the caller of this function).
The full amount that is in the process of being withdrawn is always withdrawn.
```solidity Solidity theme={null}
function withdraw(address token) external;
```
**Parameters**
| Name | Type | Description |
| ------- | --------- | ------------------------------------ |
| `token` | `address` | The address of the token to withdraw |
### GatewayMinter
#### gatewayMint
Mint funds via a signed attestation from the Gateway API. Emits an event
containing the `keccak256` hash of the encoded `TransferSpec` (which is the same
for the corresponding burn that will happen on the source domain), to be used as
a cross-chain identifier and for replay protection.
```solidity Solidity theme={null}
function gatewayMint(bytes memory attestationPayload, bytes memory signature) external;
```
**Parameters**
| Name | Type | Description |
| -------------------- | ------- | --------------------------------------------------------------------- |
| `attestationPayload` | `bytes` | The byte-encoded attestation(s) |
| `signature` | `bytes` | The signature from a valid attestation signer on `attestationPayload` |
### Common
These methods are supported by both the `GatewayWallet` and `GatewayMinter`
contracts.
#### domain
The domain assigned to the chain this contract is deployed on.
```solidity Solidity theme={null}
function domain() public view returns (uint32);
```
**Returns**
| Name | Type | Description |
| -------- | -------- | -------------------------------------------------- |
| `` | `uint32` | The Circle-issued identifier for the current chain |
#### isTokenSupported
Whether or not a token is supported.
```solidity Solidity theme={null}
function isTokenSupported(address token) public view returns (bool);
```
**Parameters**
| Name | Type | Description |
| ------- | --------- | --------------------------------- |
| `token` | `address` | The address of the token to check |
**Returns**
| Name | Type | Description |
| -------- | ------ | --------------------------------------------------- |
| `` | `bool` | `true` if the token is supported, `false` otherwise |
#### isTransferSpecHashUsed
Whether or not a transfer spec hash has been used.
```solidity Solidity theme={null}
function isTransferSpecHashUsed(bytes32 transferSpecHash) public view returns (bool);
```
**Parameters**
| Name | Type | Description |
| ------------------ | --------- | ------------------------------- |
| `transferSpecHash` | `bytes32` | The transfer spec hash to check |
**Returns**
| Name | Type | Description |
| -------- | ------ | ----------------------------------------------------------------- |
| `` | `bool` | `true` if the transfer spec hash has been used, `false` otherwise |
#### isDenylisted
Whether or not a given address is denied from interacting with the contract.
```solidity Solidity theme={null}
function isDenylisted(address addr) public view returns (bool);
```
**Parameters**
| Name | Type | Description |
| ------ | --------- | -------------------- |
| `addr` | `address` | The address to check |
**Returns**
| Name | Type | Description |
| -------- | ------ | ------------------------------------------------------ |
| `` | `bool` | `true` if the address is denylisted, `false` otherwise |
## Events
### GatewayWallet
#### Deposited
Emitted when a deposit is made. The `sender` will always be the same as
`depositor`, except when a deposit is made using `depositFor`.
```solidity Solidity theme={null}
event Deposited(address indexed token, address indexed depositor, address indexed sender, uint256 value);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ----------------------------------------------------- |
| `token` | `address` | The address of the token that was deposited |
| `depositor` | `address` | The address that the resulting balance is credited to |
| `sender` | `address` | The address that the funds were deposited from |
| `value` | `uint256` | The amount that was deposited |
#### DelegateAdded
Emitted when a delegate is authorized for a depositor's balance.
```solidity Solidity theme={null}
event DelegateAdded(address indexed token, address indexed depositor, address delegate);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ---------------------------------------------------------------- |
| `token` | `address` | The address of the token that the delegate is now authorized for |
| `depositor` | `address` | The depositor who added the delegate |
| `delegate` | `address` | The delegate that was added |
#### DelegateRemoved
Emitted when a delegate's authorization is revoked.
```solidity Solidity theme={null}
event DelegateRemoved(address indexed token, address indexed depositor, address delegate);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ----------------------------------------------------------------- |
| `token` | `address` | The address of the token the delegate is no longer authorized for |
| `depositor` | `address` | The depositor who removed the delegate |
| `delegate` | `address` | The delegate that was removed |
#### WithdrawalInitiated
Emitted when a withdrawal is initiated.
```solidity Solidity theme={null}
event WithdrawalInitiated(
address indexed token,
address indexed depositor,
uint256 value,
uint256 remainingAvailable,
uint256 totalWithdrawing,
uint256 withdrawalBlock
);
```
**Parameters**
| Name | Type | Description |
| -------------------- | --------- | -------------------------------------------------------------- |
| `token` | `address` | The address of the token that is being withdrawn |
| `depositor` | `address` | The owner of the funds being withdrawn |
| `value` | `uint256` | The value that was added to the in-progress withdrawal |
| `remainingAvailable` | `uint256` | The remaining available balance after the withdrawal |
| `totalWithdrawing` | `uint256` | The total value that is now being withdrawn |
| `withdrawalBlock` | `uint256` | The block number at which the full withdrawal can be completed |
#### WithdrawalCompleted
Emitted when a withdrawal is completed and funds have been transferred to the
depositor.
```solidity Solidity theme={null}
event WithdrawalCompleted(address indexed token, address indexed depositor, uint256 value);
```
**Parameters**
| Name | Type | Description |
| ----------- | --------- | ------------------------------------------- |
| `token` | `address` | The address of the token that was withdrawn |
| `depositor` | `address` | The owner of the withdrawn funds |
| `value` | `uint256` | The value that was withdrawn |
#### GatewayBurned
Emitted when Circle burns tokens that have been minted on another domain
```solidity Solidity theme={null}
event GatewayBurned(
address indexed token,
address indexed depositor,
bytes32 indexed transferSpecHash,
uint32 destinationDomain,
bytes32 destinationRecipient,
address signer,
uint256 value,
uint256 fee,
uint256 fromAvailable,
uint256 fromWithdrawing
);
```
**Parameters**
| Name | Type | Description |
| ---------------------- | --------- | ---------------------------------------------------- |
| `token` | `address` | The address of the token that was burned |
| `depositor` | `address` | The depositor who owned the balance |
| `transferSpecHash` | `bytes32` | The `keccak256` hash of the `TransferSpec` |
| `destinationDomain` | `uint32` | The domain the corresponding attestation was used on |
| `destinationRecipient` | `bytes32` | The recipient of the funds at the destination |
| `signer` | `address` | The address that authorized the transfer |
| `value` | `uint256` | The value that was burned |
| `fee` | `uint256` | The fee charged for the burn |
| `fromAvailable` | `uint256` | The value burned from the `available` balance |
| `fromWithdrawing` | `uint256` | The value burned from the `withdrawing` balance |
### GatewayMinter
#### AttestationUsed
Emitted when an attestation is used.
```solidity Solidity theme={null}
event AttestationUsed(
address indexed token,
address indexed recipient,
bytes32 indexed transferSpecHash,
uint32 sourceDomain,
bytes32 sourceDepositor,
bytes32 sourceSigner,
uint256 value
);
```
**Parameters**
| Name | Type | Description |
| ------------------ | --------- | ----------------------------------------------------------------------- |
| `token` | `address` | The address of the token that was minted |
| `recipient` | `address` | The recipient of the funds |
| `transferSpecHash` | `bytes32` | The `keccak256` hash of the `TransferSpec`, shared with the burn intent |
| `sourceDomain` | `uint32` | The domain the funds came from |
| `sourceDepositor` | `bytes32` | The depositor on the source domain |
| `sourceSigner` | `bytes32` | The signer that authorized the transfer |
| `value` | `uint256` | The amount that was minted |
### Common
These events are emitted by both the `GatewayWallet` and `GatewayMinter`
contracts.
#### Denylisted
Emitted when an address is added to the denylist.
```solidity Solidity theme={null}
event Denylisted(address indexed addr);
```
**Parameters**
| Name | Type | Description |
| ------ | --------- | ----------------------------------------------------------------------- |
| `addr` | `address` | The address that is now being denied from interacting with the contract |
#### UnDenylisted
Emitted when an address is removed from the denylist.
```solidity Solidity theme={null}
event UnDenylisted(address indexed addr);
```
**Parameters**
| Name | Type | Description |
| ------ | --------- | --------------------------------------------------------------- |
| `addr` | `address` | The address that is allowed to interact with the contract again |
#### TokenSupported
Emitted when a token is added to the set of supported tokens.
```solidity Solidity theme={null}
event TokenSupported(address token);
```
**Parameters**
| Name | Type | Description |
| ------- | --------- | ---------------------------------------------- |
| `token` | `address` | The address of the token that is now supported |
# ERC-1271 programmable authorization
Source: https://developers.circle.com/gateway/references/erc-1271
How Gateway validates smart contract signatures with ERC-1271, including the offchain TEE and RPC quorum security model, usage, and limitations.
Gateway supports programmable authorization through
[ERC-1271](https://eips.ethereum.org/EIPS/eip-1271). Smart contracts and smart
contract accounts (SCAs) can authorize transfers directly with their own
validation logic. You can access a unified USDC balance from a multisig,
role-based permission system, passkey, administrator key, or any other
deterministic authorization model.
## What ERC-1271 enables
An Externally Owned Account (EOA) proves authorization with a cryptographic
signature from its associated private key. Because smart contracts don't have a
private key, they can't produce signatures in the same way. ERC-1271 solves this
with a standard function `isValidSignature(bytes32 hash, bytes signature)` that
contracts can implement to decide whether a signature is valid. Verifiers call
the validation function instead of recovering the cryptographic signer directly.
This enables more complex authorization logic that can be programmed into the
smart contract. Common examples include:
* Multisig wallets that require a threshold of signers
* Role-based or policy-based permission systems
* Passkey-based smart accounts
* Contracts that enforce allowlists, spending limits, or compliance checks
before approving an action
Gateway can validate ERC-1271 signatures directly, so you don't need to add a
delegate or redesign how your application authorizes transfers.
## How ERC-1271 validation works
You submit a burn intent to the Gateway
[`/v1/transfer`](/api-reference/gateway/all/create-transfer-attestation)
endpoint with the `contractSigner: true` flag and the contract's signature.
Gateway routes the request to a validation service, deployed as an AWS Nitro
Enclave that validates the signature at request time. The enclave queries
multiple independent blockchain RPC providers and simulates the contract's
`isValidSignature` response against a recent target block height.
If a quorum of RPCs (at least 2 of 3) agree that the signature is valid, the
validation service signs off on the request and the Gateway API returns the
attestation as usual.
When the attestation is used, Gateway performs the burn using the validation
service's signature. The [Gateway Wallet contract](/gateway/references/technical-guide#gateway-wallet-contracts) recognizes the
validation service signature and completes burns validated this way.
From your application's perspective, the request and response are the same as an
EOA transfer. The Trusted Execution Environment (TEE) and RPC quorum are
internal mechanisms that let Gateway extend its offchain validation guarantee to
contract signatures.
## Security model
ERC-1271 validation runs inside a Trusted Execution Environment (TEE) so that
Circle can't unilaterally control which signatures are accepted. The security
model relies on three components.
### Trusted execution environment (TEE)
A Trusted Execution Environment (TEE) is an isolated hardware enclave that runs
code in a secure, tamper-proof environment. Gateway uses an AWS Nitro Enclave to
validate the ERC-1271 signature, query the RPC quorum, and sign the burn intent
with the enclave's private key.
The enclave's signing key is protected by AWS Key Management Service (KMS) with
attestation-based access policies. Only the audited enclave image can access the
key. Circle can't access the signing key or extract it outside the enclave.
### Independent RPC quorum
The enclave validates each signature against a quorum of independent blockchain
RPC providers. Circle can't independently modify the quorum logic or the set of
RPC providers the enclave uses, because both are fixed in the audited enclave
image. In particular, Circle can't make its own RPCs the sole validator of a
request: a request is only accepted when a quorum of the enclave's independent
providers agree that the signature is valid.
### Cryptographic attestations
AWS Nitro Enclaves produce cryptographic attestation documents that prove the
enclave is running a specific, audited code image. These attestations can be
independently verified, providing transparency into the validation process.
## Opting into ERC-1271 validation
Requests that opt into ERC-1271 validation include `contractSigner: true`
alongside the burn intent and signature in each item of the
[`/v1/transfer`](/api-reference/gateway/all/create-transfer-attestation)
payload:
```json theme={null}
[
{
"burnIntent": { "...": "..." },
"signature": "0x...",
"contractSigner": true
}
]
```
The burn intent's `sourceSigner` is the address of the signing contract, and the
`signature` is produced by that contract's ERC-1271 authorization logic. When
`contractSigner` is omitted or `false`, Gateway validates the signature as a
standard EOA signature.
For a complete walkthrough that signs a burn intent with a Circle smart contract
account, see
[How-to: Transfer USDC from a smart contract account](/gateway/howtos/transfer-with-erc-1271).
## Limitations and considerations
* **EVM-only:** ERC-1271 validation is supported only on EVM blockchains.
* **Read-only validation:** The validation service simulates `isValidSignature`
offchain, so authorization logic that modifies onchain state during validation
isn't supported.
* **Timing:** The validation service ensures that simulation is done against
recent blocks. The validation service can validate signatures using blocks up
to 5 minutes old. This means that it may take up to 5 minutes for key rotation
or revocation transactions to apply.
* **RPC trust assumptions:** Gateway uses a quorum of multiple node operators on
each request to mitigate incorrect responses, but Gateway can't guarantee that
each RPC performed the validation correctly or that an RPC's network security
wasn't compromised.
* **You remain responsible for your signing setup:** As with EOAs, using Gateway
with ERC-1271 doesn't guarantee that your contract's authorization logic is
secure. Depositors using insecure ERC-1271 implementations can have their
balances drained. Be sure to audit and secure your signing setup.
Nanopayments, part of Circle Agent Stack, isn't available through ERC-1271.
Nanopayment burn intents are batched and submitted through ERC-3009 requests,
which use a different validation path.
# Gateway fees
Source: https://developers.circle.com/gateway/references/fees
Understanding Gateway transfer fees and gas costs
Gateway charges a transfer fee and a gas fee for crosschain transfers. If you
use the [Circle Forwarding Service](/gateway/references/forwarding-service) to
handle the destination mint, Gateway also charges a forwarding fee.
## Transfer fee
Crosschain transfers incur a percentage-based fee of **0.005%** (0.5 basis
points) on the transfer amount.
* **When charged**: On crosschain transfers (source and destination are
different blockchains)
* **Payment method**: Deducted from your unified USDC balance at the time of
burn
Same-chain transfers (withdrawals where source and destination are the same
blockchain) do not incur the transfer fee.
## Gas fees
Each burn intent includes a gas fee that covers the cost of executing the burn
transaction on the source blockchain. The gas fee varies by source blockchain:
| Source blockchain | Gas fee (USDC) |
| ----------------- | -------------- |
| Arbitrum | \$0.01 |
| Avalanche | \$0.02 |
| Base | \$0.01 |
| Ethereum | \$1.00 |
| HyperEVM | \$0.05 |
| OP | \$0.0015 |
| Polygon PoS | \$0.0015 |
| Sei | \$0.001 |
| Solana | \$0.15 |
| Sonic | \$0.01 |
| Unichain | \$0.001 |
| World Chain | \$0.01 |
## Forwarding fees
When you use the
[Circle Forwarding Service](/gateway/references/forwarding-service) to handle
destination chain minting, Gateway charges an additional forwarding fee. The
forwarding fee has two parts:
* **Forwarding service fee**: A flat fee per transfer
* **Forwarding gas fee**: Covers the cost of executing the mint on the
destination blockchain
The forwarding service fee is **\$0.05** per transfer on all destination
blockchains. The forwarding gas fee is approximately the same as the
[gas fee](#gas-fees) charged for a burn on the corresponding blockchain.
To estimate the forwarding fee before submitting a transfer, call the
[`/estimate`](/api-reference/gateway/all/estimate-transfer) endpoint with
`enableForwarder=true`. The returned `forwardingFee` value includes both the
service fee and the gas fee.
When the destination is Solana and
[automatic Associated Token Account (ATA) creation](/gateway/references/forwarding-service#automatic-ata-creation-for-solana)
is enabled, the forwarding fee also includes the Solana rent cost for the token
account.
For details on how the forwarding fee is collected across burn intents, see
[Fee collection](/gateway/references/forwarding-service#fee-collection) in the
Forwarding Service topic.
## Setting `maxFee`
When creating a [burn intent](/gateway/references/technical-guide#burn-intent),
set the `maxFee` field to cover the gas fee, the transfer fee, and any
forwarding fee:
```text theme={null}
maxFee ≥ gas fee + forwarding fee + (transfer amount * 0.00005)
```
For example, transferring 1,000 USDC from Base without forwarding:
* Gas fee: \$0.01
* Transfer fee: 1,000 \* 0.00005 = \$0.05
* Minimum `maxFee`: \$0.06 (60,000 in USDC subunits)
For the same transfer with the Forwarding Service enabled:
* Gas fee: \$0.01
* Forwarding fee: \$0.06 (service fee plus forwarding gas fee)
* Transfer fee: 1,000 \* 0.00005 = \$0.05
* Minimum `maxFee`: \$0.12 (120,000 in USDC subunits)
Add a buffer to your `maxFee` calculation to account for gas fee fluctuations.
## Optimizing costs
To reduce overall costs:
* **Use low-cost source blockchains**: Keep the majority of your Gateway balance
on blockchains with lower gas fees (such as OP, Polygon PoS, Sei, or Unichain)
* **Consolidate burn intents**: The gas cost of minting scales slower than
multiple individual [CCTP](/cctp) transfers. Each additional burn intent adds
approximately 60k gas to the mint transaction.
# Circle Forwarding Service for Gateway
Source: https://developers.circle.com/gateway/references/forwarding-service
Forward destination chain mints to simplify crosschain transfers
The Circle Forwarding Service is a service for Gateway 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 Gateway transfer without the Forwarding Service is a three-step process
(assuming funds already deposited):
1. Create burn intents to burn USDC on the source chain.
2. Use the burn intents to request an attestation from the Circle API.
3. Submit the attestation to the destination minter contract to mint USDC.
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.
By enabling the forwarding service, you can pay for all the costs (base fee,
transfer fee, forwarding fee) when the funds are burned on the source chain.
## Enabling the Forwarding Service
To enable the Forwarding Service, add the `enableForwarder=true` query parameter
to the [`/transfer`](/api-reference/gateway/all/create-transfer-attestation)
endpoint:
```bash theme={null}
POST /v1/transfer?enableForwarder=true
```
When this parameter is set to `true`, Gateway will handle the destination chain
minting automatically. Because forwarding submits the destination mint for you,
the `POST /v1/transfer?enableForwarder=true` response may omit top-level
`attestation` and `signature` fields. Use
[`GET /v1/transfer/{id}`](/api-reference/gateway/all/get-transfer-by-id) for the
full transfer record, including nested attestation details.
## Fees and execution
The Forwarding Fee includes two parts:
* Forwarding Service fee
* Forwarding Gas Fee
The *Forwarding Service fee* for each transfer:
| Destination chain | Service fee (USDC) |
| ----------------- | ------------------ |
| All chains | \$0.05 |
The *Forwarding Gas fee* is the amount needed to execute the mint attestation.
This cost is roughly the same gas costs as the
[burn fees](/gateway/references/fees#gas-fees).
The burn intent max fee must account for the forwarding fee.
```text theme={null}
maxFee ≥ gas fee + forwarding fee + (transfer amount * 0.00005)
```
For example, transferring 1,000 USDC from Base:
* Gas fee: \$0.01
* forwarding fee: \$0.06 (forwarding service and gas fee)
* Transfer fee: 1,000 \* 0.00005 = \$0.05
* Minimum `maxFee`: \$0.12 (120,000 in USDC subunits)
## Estimate forwarding fee
The [`/estimation`](/api-reference/gateway/all/estimate-transfer) endpoint can
be utilized by passing in the query parameter `enableForwarder=true`
```bash theme={null}
POST /v1/estimate?enableForwarder=true
```
Note that the `forwardingFee` includes the forwarding service and gas fees. If
the `maxFee` specified in the first burn intent is not enough to satisfy the
forwarding fee, the estimate endpoint will automatically bump up the max fee.
## Fee collection
Similar to the gas fee and transfer fee, forwarding fees will also be collected
when funds are burned on the source chain.
The forwarding fee collection process works as follows:
1. The forwarding fee is deducted from the `maxFee` of the burn intents in the
order they were passed to the transfer endpoint.
2. The system will take up to the `maxFee` amount from the first burn intent to
satisfy the forwarding fee.
**Example:**
If the forwarding fee is \$0.25 and the request contains three burn intents with
the following parameters:
* Burn intent 1: `maxFee` = \$0.30
* Gas and transfer fees: \$0.08
* Available for forwarding: \$0.22
* Burn intent 2: `maxFee` = \$0.15
* Gas and transfer fees: \$0.05
* Available for forwarding: \$0.10
* Burn intent 3: `maxFee` = \$0.25
* Gas and transfer fees: \$0.06
* Available for forwarding: \$0.19
The forwarding fee collection would work as follows:
1. \$0.22 is collected from burn intent 1's available `maxFee` after gas and
transfer fees
2. \$0.03 is collected from burn intent 2's available `maxFee` to satisfy the
remaining forwarding fee, totaling \$0.25
3. No forwarding fee is collected from burn intent 3 since the full forwarding
fee has already been satisfied
Note that each burn intent's `maxFee` must first cover the gas and transfer fees
before any remaining amount can be applied to the forwarding fee.
## Automatic ATA creation for Solana
When the destination chain is Solana, the transfer `destinationRecipient` must
be an initialized USDC token account. If the recipient does not already have a
USDC token account, the Forwarding Service can create an Associated Token
Account (ATA) automatically as part of the mint transaction.
To enable automatic ATA creation, include `recipientSetupOptions` in each burn
intent that requires ATA creation in the
[`/transfer`](/api-reference/gateway/all/create-transfer-attestation) and
[`/estimate`](/api-reference/gateway/all/estimate-transfer) request bodies:
| Field | Type | Description |
| :---------------------- | :-------- | :---------------------------------------------------------- |
| `includeRecipientSetup` | `boolean` | Set to `true` to request ATA creation |
| `recipientOwnerAddress` | `bytes32` | Recipient owner address for the ATA (hex-encoded, 32 bytes) |
The following example shows how to derive the ATA address, construct
`recipientSetupOptions`, and include it in the `/estimate` and `/transfer`
requests.
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
const recipientOwner = new PublicKey("RecipientOwnerAddress");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
// Derive the ATA address — use this as destinationRecipient in the transfer spec
const recipientAta = getAssociatedTokenAddressSync(
USDC_MINT,
recipientOwner,
true,
);
const destinationRecipient =
"0x" + Buffer.from(recipientAta.toBytes()).toString("hex");
const recipientOwnerHex =
"0x" + Buffer.from(recipientOwner.toBytes()).toString("hex");
// In the /estimate request body
const estimateRequest = [
{
spec: {
// ... rest of the transfer spec
destinationRecipient: destinationRecipient,
},
recipientSetupOptions: {
includeRecipientSetup: true,
recipientOwnerAddress: recipientOwnerHex,
},
},
];
// In the /transfer request body
const transferRequest = [
{
burnIntent: {
// ... rest of the burn intent
spec: {
// ... rest of the transfer spec
destinationRecipient: destinationRecipient,
},
recipientSetupOptions: {
includeRecipientSetup: true,
recipientOwnerAddress: recipientOwnerHex,
},
},
signature,
},
];
```
For a complete walkthrough, see
[Forward to a Solana destination with ATA creation](/gateway/howtos/forwarding-service#forward-to-a-solana-destination-with-ata-creation).
### Constraints
* `recipientSetupOptions` is only valid for requests using the Forwarding
Service.
* The `destinationRecipient` must equal the ATA derived from the
`recipientOwnerAddress` and the USDC token mint.
* The `recipientOwnerAddress` 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 you derive the ATA with SPL Token
helpers and the owner might be off-curve, pass `true` for the
`allowOwnerOffCurve` parameter in `getAssociatedTokenAddressSync`.
### ATA creation fees
When automatic ATA creation is enabled, the forwarding fee includes the Solana
rent cost for creating the token account. The
[`/estimate`](/api-reference/gateway/all/estimate-transfer) endpoint factors
this cost in automatically when `recipientSetupOptions` is provided. If multiple
burn intents reference the same `recipientOwnerAddress`, the ATA creation fee is
charged only once.
# Technical guide for Gateway on Solana
Source: https://developers.circle.com/gateway/references/solana
Differences between Gateway on Solana and EVM
## Overview
The Solana Gateway protocol implementation is split into two programs:
`GatewayWallet` and `GatewayMinter`. 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.
Key differences for Solana:
* Where canonical addresses are expected in the API, Solana addresses should be
represented as base58.
* Where 32-byte addresses are expected in the API, Solana addresses should be
represented as hex.
* All block height expiration fields in the API (such as `maxBlockHeight` and
`expirationBlock`) refer to the last *slot* before the signed message is
invalid.
* `BurnIntentSets` originating from Solana are not supported. Each transfer from
Solana must be specified as a separate signed `BurnIntent`.
* Solana uses a reduced attestation format to accommodate transaction size
limits.
## Message formats
Gateway on Solana uses the same message formats as on EVM for the
[`TransferSpec`](/gateway/references/technical-guide#transfer-specification) and
[`BurnIntent`](/gateway/references/technical-guide#burn-intent) messages. These
messages are described in the
[Gateway technical guide](/gateway/references/technical-guide).
Gateway on Solana uses a reduced attestation format to accommodate transaction
size limits.
For static Solana instruction and account definitions, use the onchain program
IDLs as the source of truth:
* [`GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
* [`GatewayMinter` IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl)
This page provides high-level Solana-specific guidance, including signing rules,
integration constraints, and reduced attestation concepts.
### `ReducedMintAttestation`
A mint attestation contains the details needed for the `GatewayMinter` to mint.
Mint attestations on Solana use a "reduced" encoding to account for Solana's
transaction size limits. On Solana, all attestations are
`ReducedMintAttestationSets`. This reduces ambiguity when decoding
`MintAttestations` and saves transaction size on common fields between elements
that have been hoisted to the set level.
All integer fields in the reduced attestation encoding use big-endian byte
order. In Solana payout flows, the `destinationRecipient` field must contain the
initialized USDC token account or Associated Token Account that will receive the
mint, not the recipient wallet address.
#### Reduced attestation structure
The following tables are provided as implementation guidance for Solana reduced
attestation parsing. For canonical static program interfaces, rely on the
onchain IDLs.
#### Attestation set encoding
| Field | Offset | Bytes | Description |
| :-------------------- | :----- | :---- | :----------------------------- |
| `magic` | 0 | 4 | `0x10cbb1ec` |
| `version` | 4 | 4 | Protocol version |
| `destinationDomain` | 8 | 4 | Destination domain identifier |
| `destinationContract` | 12 | 32 | Minter contract address |
| `destinationCaller` | 44 | 32 | Allowed caller address |
| `maxBlockHeight` | 76 | 8 | Expiration slot |
| `numAttestations` | 84 | 4 | Number of attestation elements |
| `attestations` | 88 | var | Array of attestation elements |
#### Attestation element encoding
| Field | Offset | Bytes | Description |
| :--------------------- | :----- | :---- | :----------------------------------- |
| `destinationToken` | 0 | 32 | Token address on destination |
| `destinationRecipient` | 32 | 32 | Recipient address |
| `value` | 64 | 8 | Amount to mint |
| `transferSpecHash` | 72 | 32 | `keccak256` hash of the TransferSpec |
| `hookDataLength` | 104 | 4 | Length of hook data |
| `hookData` | 108 | var | Hook data bytes |
Unlike standard `MintAttestation` messages, the `ReducedMintAttestation` does
not include the full `TransferSpec`. The `TransferSpec` hash is encoded directly
into the message and can be used as a unique crosschain identifier. Retrieve the
original `TransferSpec` through the
[Get transfer spec](/api-reference/gateway/all/get-transfer-spec) endpoint.
## Signing
### `BurnIntent` signing
Once a `BurnIntent` has been constructed, prepend the signing domain header
(`0xff000000000000000000000000000000`) before signing using the ed25519 keypair
associated with the source depositor. Alternatively, sign with a delegated
address.
| Field | Offset | Bytes | Description |
| :-------------- | :----- | :---- | :----------------------------------- |
| `signingDomain` | 0 | 16 | `0xff000000000000000000000000000000` |
| `burnIntent` | 16 | var | Encoded `BurnIntent` |
The signing domain header ensures:
1. The signature cannot authorize a transaction due to illegal first byte
(`0xff`).
2. The program can be upgraded to support
[Off-Chain Message Signing Format (OCMSF)](https://docs.anza.xyz/proposals/off-chain-message-signing)
without introducing ambiguity between signing payload types.
### `MintAttestation` signing
`ReducedMintAttestation` messages are signed by the Gateway API service using
EVM-style ECDSA signatures. The `keccak256` hash of the message is signed by the
Circle attestation signer according to
[EIP-191](https://eips.ethereum.org/EIPS/eip-191), using the `personal_sign`
(`0x45`) format.
## Transaction size limits
Solana transactions can be at most 1232 bytes. To maximize composability of
`gatewayMint` instructions, the protocol minimizes the data size needed to
execute mints. The `ReducedMintAttestation` message reduces the size of the
original `MintAttestation` designed for EVM by removing fields that do not need
to be validated on the destination chain. The parameter mode for `gatewayMint`
further reduces redundancy by inferring data from the execution context or
account list.
To ensure that USDC supply can be maintained 1:1, the Gateway Transfer API
rejects user `BurnIntents` that are too large to be safely executed on Solana.
Verify your transaction sizes before broadcasting transactions using the
`gatewayMint` instruction.
### Executing large mint attestations
You can execute large `MintAttestations` that exceed the transaction size limit,
as long as strict atomicity is not required. Deploy a separate program that
writes `MintAttestation` data into a buffer account across multiple
transactions. Once the message has been written to the buffer account, this
program can execute a cross-program invocation (CPI) into the Gateway Minter
program, passing in the `MintAttestation` bytes from the buffer account as an
argument. CPI calls can be at most 10 \* 1024 bytes.
# Solana programs and interfaces for Gateway
Source: https://developers.circle.com/gateway/references/solana-programs
The wallet and minter programs have a set of accounts and instructions that you
can use to build with Gateway on Solana. The following sections describe the
main [Program Derived Account (PDA) structures](#program-derived-account-pdas)
and [instructions](#instructions) for the Gateway programs.
For static Solana accounts, instruction arguments, and instruction definitions,
use the onchain IDLs as the source of truth. This page adds high-level Gateway
guidance and documents the dynamic `gatewayMint` remaining accounts that are not
represented statically in the IDL.
## Mainnet program addresses
| Program | Address |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| `GatewayWallet` | [`GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ`](https://solscan.io/account/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ?cluster=mainnet) |
| `GatewayMinter` | [`GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ`](https://solscan.io/account/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ?cluster=mainnet) |
## Devnet program addresses
| Program | Address |
| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `GatewayWallet` | [`GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu`](https://solscan.io/account/GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu?cluster=devnet) |
| `GatewayMinter` | [`GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr`](https://solscan.io/account/GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr?cluster=devnet) |
## Account structures
The following section describes the main Program Derived Account (PDA)
structures for the Gateway programs. You can find the full IDLs for the Gateway
Solana programs at the following links:
* [`GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
* [`GatewayMinter` IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl)
### `GatewayDeposit`
A Gateway Wallet data account used to track the balance of a particular
depositor and token.
| Parameter | Type | Description |
| -------------------- | -------- | ------------------------------------------------------- |
| `bump` | `u8` | The bump seed for the GatewayDeposit PDA. |
| `depositor` | `Pubkey` | The depositor of the GatewayDeposit. |
| `token_mint` | `Pubkey` | The mint of the token being deposited. |
| `available_amount` | `u64` | The available amount of the token. |
| `withdrawing_amount` | `u64` | The amount of the token pending withdrawal. |
| `withdrawal_block` | `u64` | The block number at which the withdrawal was initiated. |
### `GatewayDelegate`
A Gateway Wallet data account used to store delegates.
| Parameter | Type | Description |
| -------------------- | ---------------- | ----------------------------------------------------- |
| `bump` | `u8` | The bump seed for the GatewayDelegate PDA. |
| `status` | `DelegateStatus` | The status of the delegate. |
| `closeable_at_block` | `u64` | The block number at which the delegate can be closed. |
| `token` | `Pubkey` | The mint of the token being delegated. |
| `depositor` | `Pubkey` | The depositor of the delegate. |
| `delegate` | `Pubkey` | The delegate address. |
### `Denylist`
A Gateway Wallet data account used to store a single denylisted account. The
address being denylisted is implied in the PDA address, and the account itself
stores no additional data.
### `UsedTransferSpecHash`
A Gateway Wallet data account used to store whether a `TransferSpecHash` has
been used, to prevent replay. The used `TransferSpecHash` is implied in the PDA
address, and the account itself stores no additional data.
## PDA derivation
The following PDAs are used by the Gateway Solana programs. Seed bytes are ASCII
strings unless otherwise noted.
### `GatewayWallet` program PDAs
| PDA | Program | Seeds |
| ---------------------- | --------------- | ------------------------------------------------------- |
| `GatewayWallet` | `GatewayWallet` | `["gateway_wallet"]` |
| `GatewayWalletCustody` | `GatewayWallet` | `["gateway_wallet_custody", token_mint]` |
| `GatewayDeposit` | `GatewayWallet` | `["gateway_deposit", token_mint, depositor]` |
| `Denylist` | `GatewayWallet` | `["denylist", account]` |
| `GatewayDelegate` | `GatewayWallet` | `["gateway_delegate", token_mint, depositor, delegate]` |
### `GatewayMinter` program PDAs
| PDA | Program | Seeds |
| ---------------------- | --------------- | ------------------------------------------------- |
| `GatewayMinter` | `GatewayMinter` | `["gateway_minter"]` |
| `GatewayMinterCustody` | `GatewayMinter` | `["gateway_minter_custody", token_mint]` |
| `UsedTransferSpecHash` | `GatewayMinter` | `["used_transfer_spec_hash", transfer_spec_hash]` |
### `GatewayMinter`
Stores general configuration for the GatewayMinter program.
| Parameter | Type | Description |
| ----------------------------- | ------------- | ----------------------------------------------------- |
| `bump` | `u8` | The bump seed for the GatewayMinter PDA. |
| `owner` | `Pubkey` | The owner of the GatewayMinter. |
| `pending_owner` | `Pubkey` | The pending owner of the GatewayMinter. |
| `pauser` | `Pubkey` | The pauser of the GatewayMinter. |
| `token_controller` | `Pubkey` | The token controller of the GatewayMinter. |
| `paused` | `bool` | Whether the GatewayMinter is paused. |
| `enabled_attesters` | `Vec` | The enabled attesters of the GatewayMinter. |
| `local_domain` | `u32` | The local domain of the GatewayMinter. |
| `version` | `u32` | The version of the GatewayMinter. |
| `supported_tokens` | `Vec` | The supported tokens of the GatewayMinter. |
| `custody_token_account_bumps` | `Vec` | The custody token account bumps of the GatewayMinter. |
### `GatewayWallet`
Stores general configuration for the GatewayWallet program.
| Parameter | Type | Description |
| ----------------------------- | ------------- | ----------------------------------------------------- |
| `bump` | `u8` | The bump seed for the GatewayWallet PDA. |
| `owner` | `Pubkey` | The owner of the GatewayWallet. |
| `pending_owner` | `Pubkey` | The pending owner of the GatewayWallet. |
| `pauser` | `Pubkey` | The pauser of the GatewayWallet. |
| `denylister` | `Pubkey` | The denylister of the GatewayWallet. |
| `token_controller` | `Pubkey` | The token controller of the GatewayWallet. |
| `fee_recipient` | `Pubkey` | The fee recipient of the GatewayWallet. |
| `local_domain` | `u32` | The local domain of the GatewayWallet. |
| `version` | `u32` | The version of the GatewayWallet. |
| `withdrawal_delay` | `u64` | The withdrawal delay of the GatewayWallet. |
| `paused` | `bool` | Whether the GatewayWallet is paused. |
| `supported_tokens` | `Vec` | The supported tokens of the GatewayWallet. |
| `custody_token_account_bumps` | `Vec` | The custody token account bumps of the GatewayWallet. |
| `burn_signers` | `Vec` | The burn signers of the GatewayWallet. |
## Instructions
The following section describes the main instructions for the Gateway programs.
Instruction data uses Anchor-style `Borsh` encoding.
### `gatewayMint`
Mints tokens to a recipient on Solana based on a signed attestation produced by
the Gateway API. The instruction supports two parameter encodings:
* **Full attestation mode**: Pass the full ReducedMintAttestation message and
its ECDSA signature as returned by the Transfer API.
* **Parameter mode**: Pass only the minimal fields that cannot be inferred
onchain; the program reconstructs the attestation and verifies the ECDSA
signature against it.
For the static instruction definition, refer to the
[onchain `GatewayMinter` IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl).
The remaining accounts described below are dynamic and therefore are not
captured statically in the IDL.
#### Account list
The Gateway Mint flow uses fixed program accounts together with three additional
ordered accounts for each transfer in the attestation. Refer to the
[onchain IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl)
for the canonical static account list.
For transfers to Solana, the `destinationRecipient` must be an initialized
USDC Token Account. If the intended recipient is a standard wallet address,
use its Associated Token Account (ATA), not the recipient wallet address
itself.
| Parameter | Type | Description |
| -------------------- | ------------------------ | ------------------------------------------ |
| `payer` | `Signer` | The payer of the transaction. |
| `destination_caller` | `Signer` | The destination caller of the transaction. |
| `gateway_minter` | `Account` | The GatewayMinter account. |
| `system_program` | `Program` | The System program. |
| `token_program` | `Program` | The Token program. |
Three additional ordered accounts are required in the remaining accounts list
for each transfer in the attestation:
| Parameter | Type | Description |
| ---------------------------- | ------------------------------- | ----------------------------------------------------------- |
| `custody_token_account` | `Account` | The custody token account of the token used in the transfer |
| `destination_recipient` | `Account` | The destination recipient token account of the transfer |
| `transfer_spec_hash_account` | `Account` | The transfer spec hash account |
In the common direct-mint flow, `destination_caller` is the transaction sender
and usually matches `payer`. If the attestation sets a non-zero
`destinationCaller`, it must match the signer that submits the mint transaction.
#### Full attestation mode (`gateway_mint`)
Pass the full reduced attestation bytes and signature returned by the Transfer
API. Refer to the
[onchain IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl)
for the canonical static argument definitions for `gateway_mint` and
`gateway_mint_with_params`.
#### Parameter mode (`gateway_mint_with_params`)
Saves transaction space by assuming certain fields or reading them from the
account list. Refer to the
[onchain IDL](https://explorer.solana.com/address/GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ/idl)
for the canonical static argument definitions for `gateway_mint_with_params` and
`MintAttestationParams`.
### `deposit`
Deposits tokens into the Gateway Wallet system.
* **Deposit mode**: Deposits tokens on behalf of the signer. The tokens are
transferred from the signer's token account to the Gateway Wallet custody and
tracked in the signer's balance.
* **Deposit For mode**: Deposits tokens on behalf of another depositor. The
tokens are transferred from the signer's token account to the Gateway Wallet
custody and tracked in the specified depositor's balance.
Refer to the
[onchain `GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
for the canonical static account and argument definitions for `deposit` and
`depositFor`.
### `initiateWithdrawal`
Starts the withdrawal process. After a program-defined `withdrawal_delay` (in
slots), `withdraw` may be called to complete the withdrawal. Repeated calls add
to the amount and reset the delay timer.
Refer to the
[onchain `GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
for the canonical static account and argument definitions for
`initiateWithdrawal`.
### `withdraw`
Completes a withdrawal that was initiated at least `withdrawal_delay` earlier.
The full pending amount is withdrawn to the depositor's token account.
Refer to the
[onchain `GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
for the canonical static account definitions for `withdraw`.
### `addDelegate`
Allows a delegate to transfer the caller's balance for the specified token.
Refer to the
[onchain `GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
for the canonical static account and argument definitions for `addDelegate`.
### `removeDelegate`
Revokes a previously granted delegate for the caller's balance of the specified
token.
Revocation is respected by the Gateway API, which refuses BurnIntents signed by
a revoked delegate. However, revocation does not prevent a BurnIntent signed by
the delegate from being executed onchain. This design prevents malicious users
from blocking a burn transaction by pre-emptively revoking a signer.
Refer to the
[onchain `GatewayWallet` IDL](https://explorer.solana.com/address/GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ/idl)
for the canonical static account and argument definitions for `removeDelegate`.
# Gateway supported blockchains
Source: https://developers.circle.com/gateway/references/supported-blockchains
List of blockchains that Gateway supports
Gateway supports a subset of blockchains where USDC is natively issued. The
following sections show the chains that Gateway supports.
Circle identifies each blockchain by a numeric domain identifier. These domain
identifiers are not tied to any existing public chain ID. Gateway uses the same
domain identifiers as [CCTP](/cctp/cctp-supported-blockchains).
## Testnet
| Blockchain | Domain | Nanopayments | `SupportedChainName` |
| ------------------- | ------ | ------------ | -------------------- |
| Arc Testnet | 26 | Yes | `arcTestnet` |
| Arbitrum Sepolia | 3 | Yes | `arbitrumSepolia` |
| Avalanche Fuji | 1 | Yes | `avalancheFuji` |
| Base Sepolia | 6 | Yes | `baseSepolia` |
| Ethereum Sepolia | 0 | Yes | `sepolia` |
| HyperEVM Testnet | 19 | Yes | `hyperEvmTestnet` |
| OP Sepolia | 2 | Yes | `optimismSepolia` |
| Polygon Amoy | 7 | Yes | `polygonAmoy` |
| Sei Atlantic | 16 | Yes | `seiAtlantic` |
| Solana Devnet | 5 | No | |
| Sonic Testnet | 13 | Yes | `sonicTestnet` |
| Unichain Sepolia | 10 | Yes | `unichainSepolia` |
| World Chain Sepolia | 14 | Yes | `worldChainSepolia` |
## Mainnet
| Blockchain | Domain | Nanopayments |
| ----------- | ------ | ------------ |
| Arbitrum | 3 | Yes |
| Avalanche | 1 | Yes |
| Base | 6 | Yes |
| Ethereum | 0 | Yes |
| HyperEVM | 19 | Yes |
| OP | 2 | Yes |
| Polygon PoS | 7 | Yes |
| Sei | 16 | Yes |
| Solana | 5 | No |
| Sonic | 13 | Yes |
| Unichain | 10 | Yes |
| World Chain | 14 | Yes |
## Required block confirmations
Before updating the unified USDC balance or issuing
[attestations](/gateway/references/technical-guide#attestation), the Gateway API
waits for a specific number of block confirmations so that the deposit
transaction is unlikely to be reverted.
The following table shows the number of block confirmations and the average time
to attestation for each supported chain. The average time to attestation is the
time it takes for a transaction to be included in a block and confirmed by the
network.
| Blockchain | Number of blocks | Average time |
| ----------- | ---------------- | ------------------ |
| Arbitrum | \~65 ETH blocks | \~13 to 19 minutes |
| Arc testnet | \~1 | \~0.5 seconds |
| Avalanche | 1 | \~8 seconds |
| Base | \~65 ETH blocks | \~13 to 19 minutes |
| Ethereum | \~65 | \~13 to 19 minutes |
| HyperEVM | \~1 | \~5 seconds |
| OP | \~65 ETH blocks | \~13 to 19 minutes |
| Polygon PoS | \~2-3 | \~8 seconds |
| Sei | \~1 | \~5 seconds |
| Solana | \~2-3 | \~8 seconds |
| Sonic | \~1 | \~8 seconds |
| Unichain | \~65 ETH blocks | \~13 to 19 minutes |
| World Chain | \~65 ETH blocks | \~13 to 19 minutes |
## Fast deposits
Some deposit times may be too long for certain use cases. Third parties may
offer alternative methods for depositing funds into Gateway with faster deposit
times. For example, [Eco](https://www.eco.com) offers a
[Gateway deposit service](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits).
Circle does not endorse, maintain, or audit Eco or any other similar third-party
service. Use of any such third-party service involves risks, and you are solely
responsible for conducting your own diligence and reviewing the applicable terms
and conditions before using such service. This guide is provided solely for
informational purposes and may become outdated.
# Gateway technical guide
Source: https://developers.circle.com/gateway/references/technical-guide
Circle Gateway consists of
[two smart contracts](/gateway/references/contract-addresses) (each deployed on
multiple blockchains), and an offchain system. Users deposit USDC into Gateway
Wallet contracts on any supported source chain. Once their deposits are
finalized, they can instantly transfer their USDC to a destination chain through
an API call, followed by a contract call to the Gateway Minter on the
destination chain. The contracts, together with the offchain system, ensure the
integrity of the USDC supply and prevent double-spending.
Compared to [CCTP](/cctp), Gateway allows users to front-load the finalization
wait time rather than requiring finality in the middle of a transfer flow. This
enables users to use all of their USDC instantly on any blockchain (even in
amounts exceeding the balance held on any single blockchain), without needing to
decide beforehand what amount or destination is needed. As such, it is optimized
for use-cases requiring capital efficiency, low latency, and chain abstraction.
## Gateway Wallet contracts
The wallet contract accepts deposits from users via direct transfer, EIP-2612
permit, or ERC-3009 authorization. It's deployed to multiple blockchains so
users can deposit from whatever source chain they hold USDC on. The contract is
non-custodial, meaning users always have full control over their USDC and Circle
can't transfer or burn any USDC without explicit user authorization. Once USDC
is deposited and the deposit events are finalized onchain, the USDC can be used
crosschain.
To preserve the non-custodial nature of the USDC held in the Gateway Wallet
contract, there is a trustless withdrawal mechanism that may be used in the
unlikely event that Circle's APIs are down for an extended period or service is
otherwise unavailable. To withdraw USDC onchain with no API interaction, users
must first initiate a withdrawal with a transaction, wait for a 7-day withdrawal
delay period, and then the user may complete the withdrawal and receive the
USDC. This delay is what allows the Gateway System to safely issue attestations
to instantly transfer USDC to other blockchains, since it guarantees that there
is sufficient time to submit the corresponding burn transaction.
## Gateway Minter contracts
The
[minter contract](/gateway/references/contract-interfaces-and-events#gatewayminter)
accepts attestations signed by the Gateway System and mints USDC to the
specified destination. It's also deployed to multiple blockchains, including
blockchains that may not yet support the wallet contract.
## Gateway system
Circle runs an offchain, automated system that serves an API for user
interaction, observes onchain events, and ensures that every mint on a
destination chain corresponds 1:1 with a burn on each source chain.
### Balances
At the center of the system is an offchain ledger that represents the USDC
balances that are deposited and are available for use in instant transfers.
These balances are tracked for every combination of blockchain, token, and
address. They're eventually consistent with onchain state, where safety is
ensured by mechanisms built into the wallet contract.
### Inputs
The following table describes the main inputs to the Gateway System, along with
what it does in response to each.
| Input | Response |
| ------------------------------------- | ------------------------------------ |
| `Deposit` event | Increment balance |
| Transfer request (attestation issued) | Decrement balance |
| `AttestationUsed` event | Submit burn intents to source chains |
| Attestation expires unused | Increment balance |
| `WithdrawInitiated` event | Decrement balance |
All onchain events are only observed as they're finalized. See
[Required block confirmations](/gateway/references/supported-blockchains#required-block-confirmations)
for the details on finality on each supported chain.
## Core primitives
The contracts and API share a set of primitives that are used to represent
transfers consistently, both when interacting with the API and as inputs to the
smart contracts. There is also a byte encoding for each that's used across
blockchains. See the
[source code of the EVM contracts](https://github.com/circlefin/evm-gateway-contracts)
for details about widths, offsets, and type markers.
Solana uses different message encodings and signing requirements to accommodate
transaction size limits. See
[Solana programs and interfaces](/gateway/references/solana-programs) for
Solana-specific details.
### Transfer specification
A
[transfer specification](https://github.com/circlefin/evm-gateway-contracts/blob/master/src/lib/TransferSpec.sol)
describes everything about a transfer from one domain to another. It's embedded
in the other two primitives, and its `keccak256` hash is used as a crosschain
identifier and replay protection.
**`TransferSpec`**
| Field | Description |
| ---------------------- | ----------------------------------------------------------------------------- |
| `version` | The protocol version, used for forward compatibility. Always set to 1. |
| `sourceDomain` | The domain of the wallet contract from which this transfer came |
| `destinationDomain` | The domain of the minter contract from which this transfer is valid |
| `sourceContract` | The address of the wallet contract on the source domain |
| `destinationContract` | The address of the minter contract on the destination domain |
| `sourceToken` | The token address on the source domain |
| `destinationToken` | The token address on the destination domain |
| `sourceDepositor` | The address to debit in the wallet contract on the source domain |
| `destinationRecipient` | The address to receive the USDC on the destination domain |
| `sourceSigner` | The signer who signed for the transfer (may be the same as `sourceDepositor`) |
| `destinationCaller` | The address of the caller who may use the attestation (0 for any caller) |
| `value` | The amount to be transferred |
| `salt` | An arbitrary value that can make the transfer spec hash unique |
| `hookData` | Arbitrary bytes that may be used for onchain composition |
### Burn intent
A
[burn intent](https://github.com/circlefin/evm-gateway-contracts/blob/master/src/lib/BurnIntents.sol)
is constructed and signed by the user, and provided to the Gateway API during a
transfer request. This is used to authenticate the user (since it must be signed
by the address specified in the `sourceSigner` field of the transfer spec) as
well as to declare the parameters of the desired transfer.
**`BurnIntent`**
| Field | Description |
| ---------------- | ---------------------------------------------------------- |
| `maxBlockHeight` | The expiration block height on the source chain |
| `maxFee` | The maximum fee that may be collected by Circle |
| `spec` | The transfer specification describing the desired transfer |
**Note**: When Arbitrum is the source chain, the `maxBlockHeight` in the burn
intent should be in terms of the **Ethereum L1 block height**. Refer to the
[Arbitrum
Docs](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time)
to learn more about Arbitrum's behavior around block height.
**Note**: When Solana is the source chain, `maxBlockHeight` refers to the last
slot before the signed message is invalid.
This user-signed payload is what Gateway System uses to prove to the wallet
contract on the source chain that the user intended to make the specified
transfer. Without this user signature, Circle is unable to unilaterally burn or
transfer user assets from the wallet contract to complete the transfer loop. To
further cement the non-custodial nature of the wallet contract, burn intents
expire so that the Gateway System doesn't hold "active" intents for long.
At the time of the transfer request, the Gateway System verifies that the user's
signature is valid, that the expiry block is sufficiently far in the future (at
least the wallet's `withdrawalDelay` from the current block), and that the fee
is sufficient to cover both the gas for the burn transaction and the transfer
fee.
If multiple burn intents share a common `sourceSigner` and the relevant
blockchains all share the same signature scheme (such as multiple EVM
blockchains), burn intents may be packed together into a burn intent set and
signed as a single payload:
**`BurnIntentSet`**
| Field | Description |
| --------- | ------------------------------------------------------ |
| `intents` | An array of burn intents describing multiple transfers |
**Note**: You can include a maximum of 16 burn intents in a single transfer
request. The Gateway API returns a `400` error for requests that include more
than 16 burn intents.
**Note**: Burn intent sets are only supported on EVM blockchains. On Solana,
each transfer must be specified as a separate signed `BurnIntent`. See [Solana
programs and interfaces](/gateway/references/solana-programs) for details.
For EVM blockchains, this structure is signed as EIP-712 typed data. For other
blockchains, it's signed in its byte-encoded form.
#### Signatures
Gateway supports Externally Owned Account (EOA) signatures, verified with a
static ECDSA check. On EVM blockchains, it also supports smart contract
signatures, verified with ERC-1271. This lets smart contracts and smart contract
accounts (SCAs) authorize transfers directly with their own validation logic.
See [ERC-1271 programmable authorization](/gateway/references/erc-1271) for
details.
### Attestation
An
[attestation](https://github.com/circlefin/evm-gateway-contracts/blob/master/src/lib/Attestations.sol)
is constructed and signed by the Gateway System in response to a transfer
request. It proves to the minter contract that at the time of the transfer
request, the user had a sufficient balance in the wallet contracts and all other
details of the transfer were valid, so the mint is safe to perform. Attestations
expire after 10 minutes.
**`Attestation`**
| Field | Description |
| ---------------- | ---------------------------------------------------------- |
| `maxBlockHeight` | The expiration block height on the destination chain |
| `spec` | The transfer specification describing the desired transfer |
**Note**: When Arbitrum is the destination chain, the `maxBlockHeight` in the
attestation is returned in terms of the **Ethereum L1 block height**. Refer to
the [Arbitrum
Docs](https://docs.arbitrum.io/build-decentralized-apps/arbitrum-vs-ethereum/block-numbers-and-time)
to learn more about Arbitrum's behavior around block height.
The transfer specification contained in an attestation is identical to what was
signed by the user. Because of this, its `keccak256` hash is emitted during the
mint transaction and the same hash is emitted during the corresponding burn
transaction. This allows for crosschain traceability and reconciles the 1:1
relationship between mints and burns in the system.
When a transfer involves multiple source domains (such as when the user passes a
burn intent set or multiple standalone burn intents), the Gateway System
constructs and signs an attestation set:
**`AttestationSet`**
| Field | Description |
| -------------- | ------------------------------------------------------ |
| `attestations` | An array of attestations describing multiple transfers |
While events are emitted for each attestation contained in a set, the entire
transaction is atomic and only one mint (of the total value of all contained
attestations) happens during the transaction.
## Key operations
The Gateway System supports the following key operations.
### Deposit
To deposit USDC into the Gateway System, users may choose between several
onchain deposit methods:
| Method | Description |
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
| [`deposit`][deposit] | Deposit to the contract after granting an allowance for the token |
| [`depositFor`][depositFor] | Same as `deposit`, but credit the USDC to another depositor's balance |
| [`depositWithPermit`][depositWithPermit] | Deposit using a signed EIP-2612 permit (credited to the signer) |
| [`depositWithAuthorization`][depositWithAuthorization] | Deposit using a signed ERC-3009 authorization (credited to the signer) |
[deposit]: /gateway/references/contract-interfaces-and-events#deposit
[depositFor]: /gateway/references/contract-interfaces-and-events#depositfor
[depositWithPermit]: /gateway/references/contract-interfaces-and-events#depositwithpermit
[depositWithAuthorization]: /gateway/references/contract-interfaces-and-events#depositwithauthorization
**Warning:** Directly transferring USDC to the Gateway Wallet contract with a
standard ERC-20 transfer will result in loss of that USDC. You must use one of
the deposit methods on the wallet contract to get a unified USDC balance.
It's important to note that before deposited USDC shows up in the Gateway System
and may be used for transfers, the deposit transactions must be finalized
onchain. See
[Required block confirmations](/gateway/references/supported-blockchains#required-block-confirmations)
for more details about what's considered finalized on each supported blockchain.
### Check balances
Use the `/v1/balances` endpoint of the API to check the latest available balance
recorded by the Gateway System. These balances are what's available to be
instantly transferred using a transfer request.
### Check pending deposits
Use the [`/v1/deposits`](/api-reference/gateway/all/get-deposits) endpoint to
check pending deposits. A deposit is in `pending` state when the user has
submitted the deposit transaction but it has not yet been processed by the
Gateway service.
### Instant transfer
First, construct a burn intent or burn intent set describing the desired
transfer. Sign it with the address that owns the USDC or is an authorized
delegate (see below for more information about delegates).
Pass the burn intents to the `/v1/transfer` endpoint of the API to request an
attestation from the Gateway System. If the transfer is valid and the request is
not forwarded, the API responds with top-level `attestation` and `signature`
fields for the destination domain.
Forwarded requests may omit those fields. To retrieve the richer transfer
record, including `burnIntents` and nested `attestation.payload` and
`attestation.signature` fields, use
[`GET /v1/transfer/{id}`](/api-reference/gateway/all/get-transfer-by-id).
Next, make a contract call to the minter contract on the destination chain using
the attestation and signature when mint submission is not being forwarded. To
atomically compose this mint with other onchain actions, use a multi-call
contract.
Note that if `destinationCaller` is specified in any of the transfer specs, it
must match the sender of the transaction (this can be used to prevent
front-running a mint when it's intended to be composed with other actions in the
same transaction).
The Gateway System ensures that if an attestation is used, the corresponding
burn transaction is submitted to all involved source chains.
### Withdrawal
There are two ways to remove USDC from the Gateway system:
[instant transfers](#instant) and [trustless withdrawals](#trustless).
#### Instant
The destination chain of a transfer may be the same as the source chain. This
means that to withdraw USDC from the wallet contract on the same blockchain, the
transfer flow described in the preceding section applies. The only
[fee](/gateway/references/fees) for same chain withdrawals is to cover gas for
the burn transaction (no other transfer fee is charged).
The reason same chain withdrawals still involve a mint and burn rather than just
a transfer out of the wallet contract is to ensure that there are no methods of
removing USDC from the wallet contract that don't involve a user signature.
#### Trustless
In the unlikely event that Circle's APIs are down for an extended period or
service is unavailable for any other reason, users can trustlessly withdraw USDC
with a delay.
First, make a contract call to the
[`initiateWithdrawal`](/gateway/references/contract-interfaces-and-events#initiatewithdrawal)
method with the desired amount to withdrawal. After a delay of 7 days, make a
contract call to the
[`withdraw`](/gateway/references/contract-interfaces-and-events#withdraw) method
to complete the withdrawal.
### Delegates
Gateway lets you delegate your deposit so that a different account can authorize
transfers on your behalf. Any user can add one or more other addresses that are
allowed to sign transfer requests for their deposited USDC. This acts as a full
allowance for that balance.
The delegate account can be any account whose signatures Gateway accepts,
including an EOA or a smart contract using
[ERC-1271](/gateway/references/erc-1271). This is built into Gateway through its
[delegate mechanism](https://github.com/circlefin/evm-gateway-contracts/blob/master/src/modules/wallet/Delegation.sol).
#### Adding a delegate
To add a delegate, use the
[`addDelegate`](/gateway/references/contract-interfaces-and-events#adddelegate)
method on the wallet contract. This needs to be done for each wallet contract
where the depositor address holds USDC across all blockchains.
#### Removing a delegate
To remove a delegate (for key rotation or other purposes), use the
[`removeDelegate`](/gateway/references/contract-interfaces-and-events#removedelegate)
method of the wallet contract. This needs to be done for each wallet contract
where the depositor address holds USDC across all blockchains.
Note that when delegates are removed, signatures they produced are still valid
for the purpose of fulfilling burn intents onchain. This ensures that burns may
be executed safely even in the event of a revocation. The API still reflects
revocations as soon as they're finalized onchain.
## Implementation considerations
When building directly with Gateway contracts and APIs, keep these safety checks
in your application flow:
* Use the Gateway Wallet deposit methods only. Do not expose the Gateway Wallet
contract as a generic ERC-20 recipient; standard ERC-20 transfers to the
Gateway Wallet contract result in loss of USDC.
* Prefer signature-based or bounded authorization for deposits where possible.
Use `depositWithPermit` or `depositWithAuthorization` when the token and
wallet support them. If your flow uses `approve` followed by `deposit`, treat
them as two separate transactions and surface their status separately.
* Treat pending deposits separately from available balance. Deposits must be
finalized and processed by the Gateway System before they can be used for
instant transfers.
* Treat delegates as high-privilege signers. A delegate can sign transfer
requests for deposited USDC on the wallet contract where it is authorized. Add
delegates only where needed, monitor them, and remove them during key rotation
or access removal.
* Do not assume delegate removal cancels signed burn intents. Burn intents that
were signed before revocation remain valid for onchain execution until they
expire.
* For ERC-1271 contract signers, see
[ERC-1271 programmable authorization](/gateway/references/erc-1271#limitations-and-considerations).
* Track expiry and transfer records. Burn intents expire according to
`maxBlockHeight`, and Gateway attestations expire if unused. For forwarded
transfers, store the returned transfer `id` and use `GET /v1/transfer/{id}` to
check status.
* Use `destinationCaller` for composed mint flows where only a specific caller
should be able to use the attestation. This can help prevent third-party mint
front-running when the mint is intended to be part of another onchain action.
* Include the trustless withdrawal path in recovery planning. If Circle's APIs
are unavailable for an extended period, users can initiate a withdrawal and
complete it after the 7-day delay.
## Security audit
The Gateway smart contracts have been independently audited by two third-party
security firms:
* [ChainSecurity audit report](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/CCTP/%5BPublic%5D%20%5BChainSecurity%5D%20Circle_Gateway_audit.pdf)
* [OtterSec audit report](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/Circle%20Gateway%20Audit%20-%20OtterSec%20-%207-21-2025.pdf)
# Gateway webhook events
Source: https://developers.circle.com/gateway/references/webhook-events
Reference for Gateway webhook event types, payload schemas, and example notifications.
Gateway sends webhooks for events on registered wallet addresses. All webhooks
use the V2 notification format. The `notificationType` field names the event,
and the `notification` field contains event data.
```json theme={null}
{
"subscriptionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notificationId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"notificationType": "gateway.deposit.finalized",
"notification": {
"...": "Event-specific data. Schema varies by notificationType."
},
"timestamp": "2026-04-22T12:00:00.000000000Z",
"version": 2
}
```
## Deposit events
### `gateway.deposit.finalized`
Fires after tokens are deposited into a Gateway Wallet, the onchain transaction
is finalized, and Gateway processes the deposit.
### Payload fields
| Field | Type | Description |
| --------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique notification identifier |
| `walletAddress` | string | Registered wallet address that owns the funds. |
| `domain` | string | Chain domain identifier (for example, `"0"` for Ethereum) |
| `env` | string | Network environment: `"testnet"` or `"mainnet"` |
| `tokenAddress` | string | Token contract address |
| `amount` | string | Transfer amount in decimal format (for example, `"1.000000"` for 1 token) |
| `from` | string | Address that initiated the onchain deposit. May be the wallet owner or a delegate acting on their behalf. |
| `to` | string | Destination address |
| `txHash` | string | Onchain transaction hash |
### Example: `gateway.deposit.finalized`
```json theme={null}
{
"subscriptionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notificationId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"notificationType": "gateway.deposit.finalized",
"notification": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"walletAddress": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"domain": "0",
"env": "mainnet",
"tokenAddress": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"amount": "1000.000000",
"from": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"to": "0x19330d10d9cc8751218eaf51e8885d058642e08a",
"txHash": "0x9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b"
},
"timestamp": "2026-04-22T12:05:00.000000000Z",
"version": 2
}
```
## Mint events
The `gateway.mint.finalized` and `gateway.mint.forwarded` events share a payload
schema. Mint event payloads include an `attestations` array. Each burn intent in
a transfer maps to one attestation entry. A single transfer can include multiple
burn intents, each with its own recipient and amount.
### `gateway.mint.finalized`
Fires when an attestation is used and tokens are minted on the destination
blockchain. The `wasForwarded` field shows whether the transfer used the
[forwarding service](/gateway/references/forwarding-service).
### `gateway.mint.forwarded`
Fires when a forwarded mint relay is confirmed. This event only occurs for
transfers that use the
[forwarding service](/gateway/references/forwarding-service).
### Payload fields
| Field | Type | Description |
| --------------- | ------- | ----------------------------------------------------------------------------------------- |
| `transferId` | string | Transfer identifier (attestation group ID) |
| `txHash` | string | Onchain transaction hash on the destination blockchain |
| `walletAddress` | string | The depositor that owns the funds on the source chain |
| `domain` | string | Blockchain domain identifier for the destination blockchain (for example, `"7"` for Base) |
| `env` | string | Network environment: `"testnet"` or `"mainnet"` |
| `tokenAddress` | string | Token contract address on the destination blockchain |
| `wasForwarded` | boolean | Whether the transfer used the forwarding service (`gateway.mint.finalized` only) |
| `attestations` | array | Array of attestation details (see below) |
**Attestation fields**
| Field | Type | Description |
| ------------------ | ------ | ---------------------------------------------------------------------- |
| `from` | string | The depositor address that triggered the burn on the source blockchain |
| `to` | string | Recipient address for this attestation |
| `amount` | string | Amount in decimal format (for example, `"1.000000"` for 1 token) |
| `transferSpecHash` | string | Transfer specification hash for this attestation |
### Example: `gateway.mint.finalized`
```json theme={null}
{
"subscriptionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notificationId": "f6a7b890-1234-5678-cdef-901234567890",
"notificationType": "gateway.mint.finalized",
"notification": {
"transferId": "a7b89012-3456-7890-abcd-ef1234567890",
"txHash": "0x1ea922932e2bbefab697b931aa423a6ebe4a683947a5bd105bb38958e52017a5",
"walletAddress": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"domain": "7",
"env": "mainnet",
"tokenAddress": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"wasForwarded": false,
"attestations": [
{
"from": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"to": "0x7777777700000000000000000000000000000001",
"amount": "93516.491888",
"transferSpecHash": "0x4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b"
}
]
},
"timestamp": "2026-04-22T12:10:00.000000000Z",
"version": 2
}
```
### Example: `gateway.mint.forwarded`
This event fires only for transfers that use the forwarding service.
```json theme={null}
{
"subscriptionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notificationId": "0a1b2c3d-4e5f-6789-abcd-ef0123456789",
"notificationType": "gateway.mint.forwarded",
"notification": {
"transferId": "b8c90123-4567-8901-abcd-ef2345678901",
"txHash": "0x2bc033043d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a",
"walletAddress": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"domain": "3",
"env": "mainnet",
"tokenAddress": "0xcccccccccccccccccccccccccccccccccccccccc",
"attestations": [
{
"from": "0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"to": "0x8888888800000000000000000000000000000002",
"amount": "50000.000000",
"transferSpecHash": "0x5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c"
}
]
},
"timestamp": "2026-04-22T12:15:00.000000000Z",
"version": 2
}
```
# Gateway webhooks
Source: https://developers.circle.com/gateway/webhooks
Get Gateway event updates for your registered wallet addresses.
Use Gateway webhooks to get updates for your registered wallet addresses. Events
include Gateway Wallet deposits and mints on destination blockchains. Gateway is
fully permissionless, but webhook subscriptions require a free
[Circle Console](https://console.circle.com) account and API key. You do not
need billing or a credit card.
To set up an endpoint and subscribe, see
[Set up a webhook endpoint](/api-reference/webhook-endpoints). For the event
model, delivery behavior, and signature verification, see the
[Webhooks](/api-reference/webhooks) group in the API reference.
## Authentication
While Gateway itself is permissionless and does not require authentication,
webhook subscriptions require a Circle Console account and API key. Manage
subscriptions through the `POST /v2/notifications/subscriptions/permissionless`
endpoint. Create a free account at the
[Circle Console](https://console.circle.com) to get started. An API key is only
needed for managing webhook subscriptions, not for using Gateway's core transfer
features.
## Event types
Gateway webhooks currently support these event types:
* `gateway.deposit.finalized`: Tokens were deposited into a Gateway Wallet.
Fires after the deposit transaction is finalized onchain and processed by
Gateway.
* `gateway.mint.finalized`: Tokens were minted on the destination blockchain.
Fires after the mint is finalized and processed by Gateway.
* `gateway.mint.forwarded`: A forwarded mint relay was confirmed. Fires only for
forwarded transfers.
On instant-finality blockchains, the forwarded status moves directly from
pending to finalized, skipping the confirmed state. Use a combination of
`gateway.mint.finalized` and `gateway.mint.forwarded` to track the full
transfer lifecycle across all blockchains.
Omit `notificationTypes` or use `gateway.*` to receive all Gateway event types,
including any new types added in the future.
* `gateway.*` (all types): the API returns `"restricted": false`
* Subset of types: the API returns `"restricted": true`
### Example: deposit and mint lifecycle
The following example shows when events fire during a deposit and mint flow.
1. A user deposits tokens into a Gateway Wallet on a source blockchain →
**`gateway.deposit.finalized`**
2. If the transfer uses the
[forwarding service](/gateway/references/forwarding-service), the relay is
confirmed → **`gateway.mint.forwarded`**
3. The deposit is attested and tokens are minted on the destination blockchain →
**`gateway.mint.finalized`**
## Limits
| Resource | Limit |
| ------------------------------------------ | ----- |
| Subscriptions per developer account | 20 |
| Registered addresses per developer account | 50 |
## Environment
Gateway webhooks work with testnet and mainnet API keys. Use `TEST` for testnet.
Use `LIVE` for mainnet. You can create both from the same
[Circle Developer Console](https://console.circle.com) account.
## Get started
View schemas and examples for each event type
# Circle developer docs
Source: https://developers.circle.com/index
## Use cases
Add wallets and USDC payments directly in your app.
Move USDC natively between blockchains with CCTP.
Create a unified USDC balance across multiple EVM chains.
## Workflows