# Agent Stack
Source: https://developers.circle.com/agent-stack
Empower your AI agents to autonomously operate wallets, transact onchain, and pay for API services through Circle's agent-native tooling.
Circle Agent Stack lets your AI agent hold and transact USDC and other tokens
across blockchains, discover and pay for x402 services, and operate within
built-in compliance guardrails. Use with
[Claude Code](https://claude.com/claude-code), [Cursor](https://cursor.com/),
[Codex](https://openai.com/codex), [OpenClaw](https://openclaw.ai/), or any
custom AI agent.
## What you can do
* **Build with an agent-native interface**: Use
[Circle CLI](/agent-stack/circle-cli) and [Circle Skills](/ai/skills) to give
your agent access to [Circle Wallets](/wallets), [CCTP](/cctp), and
[Gateway](/gateway) from a single command interface.
* **Give your AI agent wallets**: Use
[Agent Wallets](/agent-stack/agent-wallets) to hold and spend USDC and other
tokens with customizable spending controls and built-in compliance guardrails.
* **Discover and pay for services on demand**: Search
[x402-compatible APIs](https://agents.circle.com/services) and pay per
request, without subscriptions or API keys.
* **Operate onchain across blockchains**: Trade tokens, bridge USDC, and execute
onchain strategies across
[supported blockchains](/agent-stack/agent-wallets/supported-blockchains).
## The agent stack
A command-line tool for managing agent wallets, installing skills, and
accessing the Circle product suite from any agent framework.
Wallets for AI agents with custom spending policies, multichain support, and
built-in compliance controls. Gasless across blockchains.
Gasless USDC payments at sub-cent scale. Pay for x402-compatible services
from your AI agent. Powered by Gateway.
A curated, compliance-first service catalog where AI agents can discover and
pay for USDC-priced services.
Open-source skills that give your AI agent specialized knowledge for
building with Circle products.
## Dive deeper
* Get started with the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart).
* Make your first nanopayment with the
[Agent Nanopayments quickstart](/agent-stack/agent-nanopayments/quickstart).
* Browse [Circle CLI commands](/agent-stack/circle-cli/command-reference).
# Agent Nanopayments
Source: https://developers.circle.com/agent-stack/agent-nanopayments
Gas-free, batched USDC payments at sub-cent scale for AI agents.
Agent Nanopayments, built on [Gateway Nanopayments](/gateway/nanopayments), let
your AI agent pay for [x402](/gateway/nanopayments/concepts/x402)-compatible
services in sub-cent USDC. These payments would normally be uneconomical due to
per-payment gas costs, but batching them into a single onchain settlement makes
high-frequency machine-to-machine commerce viable.
Your agent uses [Circle CLI](/agent-stack/circle-cli) to deposit USDC into a
Gateway balance, discover services, and pay for them.
## Get started
Follow the [quickstart](/agent-stack/agent-nanopayments/quickstart) to deposit
USDC, find a service, and make a nanopayment.
## Use cases
Pay for x402-compatible APIs on a per-request basis. No subscriptions or
per-service sign-ups.
Pay for compute, data, or storage at usage scale. Sub-cent payments make
granular billing viable.
Enable machine-to-machine payments at high frequency, with batched
settlement keeping costs predictable.
Find x402-compatible services on [Circle Agent
Marketplace](https://agents.circle.com/services) and pay using your [agent
wallet](/agent-stack/agent-wallets).
## Features
Gateway batches many payment authorizations and settles them onchain in a
single transaction, amortizing gas across thousands of payments. Your agent
pays no per-transaction gas. Learn more about [batched
settlement](/gateway/nanopayments/concepts/batched-settlement).
Nanopayments use the [x402 standard](/gateway/nanopayments/concepts/x402),
an open HTTP-native payment protocol built around the `402 Payment Required`
status code. Sellers declare payment requirements, your agent signs a
payment payload, and the exchange happens in a single request-response
cycle.
Your nanopayments balance can live on any [Gateway-supported
blockchain](/gateway/references/supported-blockchains).
To integrate x402 and Gateway Nanopayments in your application code instead of
Circle CLI, see the [Nanopayments buyer
quickstart](/gateway/nanopayments/quickstarts/buyer).
# Nanopayment Operations
Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations
Tasks your agent can perform using Circle CLI to deposit USDC, pay for services, and withdraw funds.
Nanopayment operations are tasks your agent can perform using
[Circle CLI](/agent-stack/circle-cli). Full command syntax is in the
[CLI Command Reference](/agent-stack/circle-cli/command-reference).
The following table describes common nanopayment operations and the CLI commands
to perform them.
| Operation | What it does | Command |
| :------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------- | :------------------------ |
| [Deposit for nanopayments](/agent-stack/agent-nanopayments/operations/deposit) | Add USDC to your Gateway balance to fund nanopayments. | `circle gateway deposit` |
| [Pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service) | Discover and pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API services using USDC. | `circle services pay` |
| [Withdraw your balance](/agent-stack/agent-nanopayments/operations/withdraw) | Move remaining USDC from your Gateway balance back to your agent wallet on the same blockchain. | `circle gateway withdraw` |
# How to: Deposit for Nanopayments
Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/deposit
Deposit USDC into Gateway to enable gas-free, sub-cent payments powered by Circle Gateway.
Deposit USDC into Gateway once, then make payments without incurring gas costs
on each transaction.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Steps
Deposit USDC with your amount, wallet address, and blockchain. Direct
deposits work from any
[Gateway-supported blockchain](/gateway/references/supported-blockchains):
```bash theme={null}
circle gateway deposit --amount 5 --address 0xYourWalletAddress --chain BASE --method direct
```
Use `--method eco` for fast deposits through Eco, which supports Base
as the source blockchain and settles balances on Polygon PoS. Eco is a
third-party fast-deposit service that Circle does not operate or
audit. Review
[Eco's docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits)
and test the flow before using it in production.
Confirm the deposit arrived:
```bash theme={null}
circle gateway balance --address 0xYourWalletAddress --chain BASE
```
The command returns your current Gateway balance. A non-zero value
confirms the deposit arrived.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options.
# How to: Pay for a Service
Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/pay-for-service
Discover and pay for x402-compatible API services using USDC.
Pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API services
directly from your agent wallet. Payment is processed before the request is
forwarded to the service.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
Most x402 services require a Gateway balance. See [Deposit for
nanopayments](/agent-stack/agent-nanopayments/operations/deposit) to set one
up.
## Steps
Browse available services at
[agents.circle.com/services](https://agents.circle.com/services), or search
by keyword from the CLI:
```bash theme={null}
circle services search "weather data"
```
To inspect the payment requirements for a specific URL before paying:
```bash theme={null}
circle services inspect https://api.example.com/weather
```
Run `circle services pay` with the service URL and your wallet details. Use
`--max-amount` to set a spending cap and avoid unexpected charges. Use
`--estimate` to preview payment requirements without paying:
```bash theme={null}
# Preview payment requirements without paying
circle services pay https://api.example.com/weather \
--address 0xYourWalletAddress \
--chain BASE \
--estimate
# Pay for the service
circle services pay https://api.example.com/weather \
--address 0xYourWalletAddress \
--chain BASE \
--max-amount 0.01
```
The CLI prints the service's response body.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including `-X` for HTTP method and `-d` for a request
body.
# How to: Withdraw Your Balance
Source: https://developers.circle.com/agent-stack/agent-nanopayments/operations/withdraw
Withdraw remaining USDC from your Gateway balance back to your agent wallet.
Withdraw remaining USDC from your Gateway balance back to your agent wallet on
the same blockchain. Useful when you're done making nanopayments and want to
recover unused funds.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
USDC is minted on the same blockchain as your Gateway balance. Crosschain
withdrawals aren't supported.
## Steps
Run `circle gateway withdraw` with the amount, your wallet address, and
the blockchain where your Gateway balance lives:
```bash theme={null}
circle gateway withdraw --amount 1 --address 0xYourWalletAddress --chain BASE
```
To send the withdrawn USDC to a different address, add `--recipient`:
```bash theme={null}
circle gateway withdraw --amount 1 --address 0xYourWalletAddress --chain BASE --recipient 0xOtherAddress
```
Not sure which blockchain your balance lives on? Run
`circle gateway balance --all` to see all blockchains.
Check that your Gateway balance decreased:
```bash theme={null}
circle gateway balance --address 0xYourWalletAddress --chain BASE
```
Then check that your wallet balance increased:
```bash theme={null}
circle wallet balance --address 0xYourWalletAddress --chain BASE
```
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options.
# Quickstart: Make a Nanopayment
Source: https://developers.circle.com/agent-stack/agent-nanopayments/quickstart
Deposit USDC into Gateway, find an x402-compatible service, and pay for it with your agent wallet.
By the end of this quickstart, your agent will have made a nanopayment to an
x402-compatible service.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Steps
Deposit 5 USDC from your wallet on Base:
```bash theme={null}
circle gateway deposit --amount 5 --address 0xYourWalletAddress --chain BASE --method direct
```
Search the Circle Agent Marketplace for available services:
```bash theme={null}
circle services search "weather"
```
Pick a service and inspect its payment requirements:
```bash theme={null}
circle services inspect https://api.example.com/weather
```
Pay with your Gateway balance on Base. Replace `0xYourWalletAddress` with
your wallet address:
```bash theme={null}
circle services pay https://api.example.com/weather \
--address 0xYourWalletAddress \
--chain BASE \
--max-amount 0.01
```
The CLI prints the service's response body. The payment settles against
your Gateway balance.
Confirm your remaining Gateway balance:
```bash theme={null}
circle gateway balance --address 0xYourWalletAddress --chain BASE
```
## Next steps
* [Pay for more services](/agent-stack/agent-nanopayments/operations/pay-for-service)
using your remaining balance.
* [Withdraw your balance](/agent-stack/agent-nanopayments/operations/withdraw)
back to your wallet when you're done.
# Agent Wallets
Source: https://developers.circle.com/agent-stack/agent-wallets
Wallets that let your agent autonomously hold, spend, trade, and earn USDC and other tokens with built-in spending controls.
Agent Wallets let your AI agent hold funds and transact onchain autonomously,
within spending policies you define. Your agent operates the wallet through
Circle CLI without writing integration code.
Built on [Circle's user-controlled wallets](/wallets/user-controlled) with
2-of-2 MPC key management, key shares are never exposed to the agent. The user
retains custody, and Circle cannot unilaterally move funds without their
involvement. All transfers are screened against sanctions controls before
submission onchain.
## Get started
Paste this prompt into your AI agent:
```text theme={null}
curl -sL https://agents.circle.com/skills/setup.md and follow the instructions to set up Circle Agent Wallet
```
The agent installs Circle CLI, creates and funds your agent wallet, and helps
you discover and pay for services with it. Prefer a manual setup? Follow the
[quickstart](/agent-stack/agent-wallets/quickstart) instead.
## Use cases
Run onchain strategies like dollar-cost averaging or token monitoring for
autonomous execution within user-defined rules.
Execute real-world tasks like booking flights or paying for subscriptions
within a scoped USDC budget.
Hold, transfer, bridge, and swap USDC across all [supported
blockchains](/agent-stack/agent-wallets/supported-blockchains) from a single
agent wallet.
Set [transfer limits, recipient allowlists, and contract
blocklists](/agent-stack/agent-wallets/wallet-operations/custom-policies)
per agent wallet. Your agent operates only within rules you define.
When running autonomous trading strategies, start with small amounts and
validate your approach before scaling. Only commit funds you are comfortable
spending.
## Features
Built on [Circle user-controlled wallets](/wallets/user-controlled). Key
shares are never exposed to the agent. Users retain custody while agents
operate in defined spending limits.
Operate wallets through Circle CLI commands from any agent framework. No
custom integration code required.
Set USDC spending limits for outbound transfers and x402 payments. Limits
can be time-bound (for example, daily, or monthly). Configure allowlists and
blocklists for wallet and contract addresses.
Pair your agent wallet with [Agent
Nanopayments](/agent-stack/agent-nanopayments) for gasless, sub-cent USDC
payments to [x402](/gateway/nanopayments/concepts/x402)-compatible services.
All transfers are screened against sanctions controls before submission
onchain. Transactions involving sanctioned entities are blocked, ensuring
agents operate within regulatory requirements.
Agent Wallets support USDC, EURC, and other ERC20 tokens, and native tokens
(for example, ETH, MATIC). USDC is the primary asset for transfers,
bridging, and x402 payments.
Agent wallet transactions are gas-sponsored. Sponsorship is capped and subject
to change. See [Fees](/agent-stack/agent-wallets/fees) for the full breakdown.
# Agent Wallet Fees
Source: https://developers.circle.com/agent-stack/agent-wallets/fees
Fee breakdown for agent wallet operations, including bridging, swapping, and payments.
The following fees may apply depending on the operations your agent wallet
performs:
| Fee | Amount | When it applies |
| :--------------------- | :------------------------------- | :--------------------------------------------- |
| Gas | \$0 (sponsored) | All onchain transactions |
| CCTP fast transfer fee | Varies by source blockchain | Bridging |
| Forwarding service fee | \$0.20 | Bridging |
| Forwarding gas fee | Varies by destination blockchain | Bridging |
| Swap provider fee | 2 bps | Swapping |
| Gateway protocol fee | 0.5 bps | Crosschain x402 payments (free for same-chain) |
| Eco deposit fee | Set by Eco | Gasless Gateway deposits using Eco |
Eco is a third-party fast-deposit service that Circle does not operate or
audit. Review [Eco's
docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits)
and test the flow before using it in production.
What to know about agent wallet fees:
* **Gas sponsorship**: Onchain transactions on agent wallets are gas-sponsored
at no cost to you. Sponsorship is capped, subject to fair use, and may change
over time.
* **CCTP fast transfer**: Agent Wallets use CCTP fast transfer only for
bridging, which provides near-instant finality at a higher fee than standard
transfers. For fast transfer rates by source blockchain, see
[CCTP fees](/cctp/concepts/fees).
* **Forwarding gas fee**: Bridging also incurs a forwarding gas fee that varies
by destination blockchain.
# Quickstart: Create an Agent Wallet
Source: https://developers.circle.com/agent-stack/agent-wallets/quickstart
Get your first agent wallet set up in minutes.
Create an agent wallet and fund it with USDC on Base.
Prefer a guided setup?
[Prompt your AI agent](/agent-stack/agent-wallets#get-started) to set it up.
Your agent can only operate the wallet if it has access to the email address
used during authentication. By default, only you receive the OTP. If you grant
your agent access to your inbox, it can authenticate on your behalf and
perform all wallet operations.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
## Steps
Follow these steps to create an agent wallet and fund it with USDC.
```bash theme={null}
circle wallet login you@example.com
```
To use testnet, add `--testnet` to the command. Sessions are stored separately
for mainnet and testnet and expire after 7 days.
On first run, Circle CLI prompts you to accept the Terms of Use and
Privacy Policy. Then Circle sends a one-time password to verify your
identity. After authentication, agent wallets are created automatically
on all supported blockchains.
```bash theme={null}
circle wallet list --type agent --chain BASE
```
Copy the wallet address returned. You will use it in the steps below.
Run `circle wallet fund` to add USDC to your wallet from another wallet you
own. Replace `0xYourWalletAddress` with the address from the previous step:
```bash theme={null}
circle wallet fund --address 0xYourWalletAddress --chain BASE --amount 10 --method crypto
```
The CLI prints a terminal QR code and an
[EIP-681](https://eips.ethereum.org/EIPS/eip-681) deposit URI. Scan the QR
code with your mobile wallet to send the funds.
To buy USDC with a card instead, pass `--method fiat` to open the onramp
provider in your browser. See
[Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund) for all
funding options.
To use testnet, replace `BASE` with a testnet blockchain (for example,
`ARC-TESTNET`) and omit `--method` and `--amount`. Testnet wallets are
auto-funded with 20 USDC from the Circle faucet. See [supported
blockchains](/agent-stack/agent-wallets/supported-blockchains) for the full
list.
Confirm the funds arrived. Replace `0xYourWalletAddress` with your wallet
address:
```bash theme={null}
circle wallet balance --address 0xYourWalletAddress --chain BASE
```
## Next steps
Now that you have a funded wallet, you can:
* [Deposit for nanopayments](/agent-stack/agent-nanopayments/operations/deposit)
and
[pay for a service](/agent-stack/agent-nanopayments/operations/pay-for-service).
* [Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer) to
another address.
* [Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge) to another
blockchain.
* [Swap tokens](/agent-stack/agent-wallets/wallet-operations/swap) using your
agent wallet.
* Explore [wallet operations](/agent-stack/agent-wallets/wallet-operations) for
more tasks.
# Agent Wallet Supported Blockchains
Source: https://developers.circle.com/agent-stack/agent-wallets/supported-blockchains
Blockchains supported by Agent Wallets on mainnet and testnet.
Agent Wallets support the following blockchains on both mainnet and testnet,
except Arc Testnet (testnet only). Pass the blockchain identifier to the
`--chain` flag in CLI commands.
Run `circle blockchain list` to retrieve the current list.
| Blockchain | Mainnet identifier | Testnet identifier |
| :---------- | :----------------: | :----------------: |
| Arbitrum | `ARB` | `ARB-SEPOLIA` |
| Arc Testnet | - | `ARC-TESTNET` |
| Avalanche | `AVAX` | `AVAX-FUJI` |
| Base | `BASE` | `BASE-SEPOLIA` |
| Ethereum | `ETH` | `ETH-SEPOLIA` |
| Monad | `MONAD` | `MONAD-TESTNET` |
| Optimism | `OP` | `OP-SEPOLIA` |
| Polygon PoS | `MATIC` | `MATIC-AMOY` |
| Unichain | `UNI` | `UNI-SEPOLIA` |
# Wallet Operations
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations
Tasks you can perform with an agent wallet using Circle CLI, including transfers, bridging, payments, and more.
Wallet operations are tasks you can perform with an agent wallet using
[Circle CLI](/agent-stack/circle-cli). Full command syntax is in the
[CLI Command Reference](/agent-stack/circle-cli/command-reference).
The following table describes common wallet operations and the CLI commands to
perform them.
| Operation | What it does | Command |
| :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :------------------------ |
| [Authenticate](/agent-stack/agent-wallets/wallet-operations/authenticate) | Sign up or log in to your agent wallet using email OTP. Creates a session valid for 7 days. | `circle wallet login` |
| [Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund) | Add funds to your agent wallet using a wallet transfer or fiat onramp. | `circle wallet fund` |
| [Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer) | Send USDC and other tokens to a designated wallet address. | `circle wallet transfer` |
| [Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge) | Move USDC from one blockchain to another using CCTP. | `circle bridge transfer` |
| [Swap tokens](/agent-stack/agent-wallets/wallet-operations/swap) | Swap one token for another directly from your agent wallet. | `circle wallet swap` |
| [Execute contract](/agent-stack/agent-wallets/wallet-operations/execute-contract) | Interact with a smart contract by calling a write function. | `circle wallet execute` |
| [Sign messages](/agent-stack/agent-wallets/wallet-operations/sign) | Sign a message or EIP-712 typed data with your wallet. | `circle wallet sign` |
| [Set policies](/agent-stack/agent-wallets/wallet-operations/custom-policies) | Set USDC transfer limits or allow/block specific recipient or contract addresses. | `circle wallet limit set` |
For deposits into Gateway, payments to x402-compatible services, and
withdrawals, see [Agent Nanopayments](/agent-stack/agent-nanopayments).
# How to: Authenticate an Agent Wallet
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/authenticate
Log in with your email to create an agent wallet session on all supported blockchains.
Authenticating creates an agent session and provisions agent wallets on all
[supported blockchains](/agent-stack/agent-wallets/supported-blockchains)
automatically. You only need to do this once per environment. Sessions last 7
days.
This is a prerequisite for all other wallet operations, including
[Fund wallet](/agent-stack/agent-wallets/wallet-operations/fund),
[Transfer USDC](/agent-stack/agent-wallets/wallet-operations/transfer),
[Bridge USDC](/agent-stack/agent-wallets/wallet-operations/bridge), and
[Pay for service](/agent-stack/agent-nanopayments/operations/pay-for-service).
Your agent can only operate the wallet if it has access to the email address
used during authentication. By default, only you receive the OTP. If you grant
your agent access to your inbox, it can authenticate on your behalf and
perform all wallet operations.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
## Steps
Choose the flow that matches your environment. Use **Interactive** when you can
respond to prompts in a terminal. Use **Non-interactive** for scripts and AI
agents that can't respond to interactive prompts.
Run `circle wallet login` with your email address:
```bash theme={null}
circle wallet login you@example.com
```
To use testnet, add `--testnet` to the command. Sessions are stored separately
for mainnet and testnet and expire after 7 days.
On first run, Circle CLI prompts you to accept the Terms of Use and
Privacy Policy.
Check your email for the one-time password from Circle. Enter it in
the terminal to verify your identity. You should see output similar
to:
```text theme={null}
Logged in as you@example.com
```
Agent wallets are created automatically on all
[supported blockchains](/agent-stack/agent-wallets/supported-blockchains).
List your wallets to find the address for the blockchain you want to
use:
```bash theme={null}
circle wallet list --type agent --chain BASE
```
Copy the address returned. You'll need it for
[funding your wallet](/agent-stack/agent-wallets/wallet-operations/fund),
[transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer),
[depositing for nanopayments](/agent-stack/agent-nanopayments/operations/deposit),
and other operations.
Run `circle wallet login` with `--init` to send the OTP and capture a
request ID. The `CIRCLE_ACCEPT_TERMS=1` prefix accepts the Circle CLI
Terms of Use and Privacy Policy so the command doesn't pause for
input on first run:
```bash theme={null}
CIRCLE_ACCEPT_TERMS=1 circle wallet login you@example.com --init
```
The CLI prints a request ID. Request IDs expire after 10 minutes and
are consumed on first use.
Pass the request ID and the OTP from your inbox:
```bash theme={null}
circle wallet login --request --otp B1X-123456
```
OTP codes are alphanumeric. Once login completes, agent wallets are
created automatically on all
[supported blockchains](/agent-stack/agent-wallets/supported-blockchains).
List your wallets to find the address for the blockchain you want to
use:
```bash theme={null}
circle wallet list --type agent --chain BASE
```
Copy the address returned. You'll need it for
[funding your wallet](/agent-stack/agent-wallets/wallet-operations/fund),
[transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer),
[depositing for nanopayments](/agent-stack/agent-nanopayments/operations/deposit),
and other operations.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options.
# How to: Bridge USDC
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/bridge
Move USDC from one blockchain to another using CCTP.
Bridge USDC across blockchains using [CCTP](/cctp). Circle handles attestation
and minting on the destination chain. You don't need a funded wallet or gas on
the destination.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Steps
Follow these steps to bridge USDC to another blockchain.
Get the estimated fee before bridging:
```bash theme={null}
circle bridge get-fee ARB --chain BASE
```
Run `circle bridge transfer` with the destination chain, amount, and your
wallet details:
```bash theme={null}
circle bridge transfer ARB --amount 10.0 --address 0xYourWalletAddress --chain BASE
```
USDC is burned on the source chain and minted on the destination. The
command returns once the bridge is complete:
```json theme={null}
{
"data": {
"message": "Bridge complete: 10.0 USDC from BASE to ARB",
"burnTxHash": "0xabc...",
"forwardTxHash": "0xdef...",
"fromChain": "BASE",
"toChain": "ARB"
}
}
```
If the bridge is still processing, you can check its status:
```bash theme={null}
circle bridge status 0xabc... --chain BASE
```
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including specifying a different recipient address on
the destination chain.
# How to: Set Spending Policies
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/custom-policies
Set transfer limits or address allowlists and blocklists on your agent wallet to control how it can move USDC and interact with contracts.
Spending policies let you cap your agent wallet's USDC transfers over a rolling
time window, or restrict the wallet to specific recipient or contract addresses.
Policies apply to mainnet agent wallets only.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate)
your agent wallet.
Spending policies require a mainnet agent wallet. Testnet is not supported.
Setting a policy triggers a second email OTP to confirm the change. The OTP is
used once and not stored.
## Steps
Choose the policy type that matches your goal.
Run `circle wallet limit set` with per-transaction, daily, weekly, and
monthly caps:
```bash theme={null}
circle wallet limit set \
--address 0xYourWalletAddress \
--chain BASE \
--policy-type stablecoin \
--per-tx 100 \
--daily 500 \
--weekly 2000 \
--monthly 5000
```
Circle sends an email OTP to your agent session email to confirm the
policy change. Limits must satisfy: per-transaction ≤ daily ≤ weekly
≤ monthly.
Confirm the limits are in effect:
```bash theme={null}
circle wallet limit --address 0xYourWalletAddress --chain BASE
```
Allowlists and blocklists restrict your wallet to specific recipient or
contract addresses.
Run `circle wallet limit set` with `--rule-type` and a bracketed,
comma-separated `--targets` list of EVM addresses. The example below
blocks transfers to two recipient addresses:
```bash theme={null}
circle wallet limit set \
--address 0xYourWalletAddress \
--chain BASE \
--policy-type stablecoin \
--rule-type recipient-blocklist \
--targets "[0xBAD1,0xBAD2]"
```
`--rule-type` accepts:
* `recipient-allowlist` / `recipient-blocklist`: allow or block USDC
transfers to specific addresses.
* `contract-allowlist` / `contract-blocklist`: allow or block contract
interactions with specific addresses.
Circle sends an email OTP to your agent session email to confirm the
policy change.
Confirm the rule is in effect:
```bash theme={null}
circle wallet limit --address 0xYourWalletAddress --chain BASE
```
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options.
# How to: Execute a Smart Contract
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/execute-contract
Call a write function on a smart contract from your agent wallet.
Execute write functions on any smart contract from your agent wallet. Common
uses include approving token allowances, interacting with DeFi protocols, or
calling custom contract logic.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Steps
Follow these steps to execute a smart contract function.
For Circle contracts (USDC, CCTP, Gateway), look up the address for your
blockchain:
```bash theme={null}
circle contract address usdc --chain BASE
```
Run `circle wallet execute` with the ABI function signature, parameters,
and contract address:
```bash theme={null}
circle wallet execute "approve(address,uint256)" 0xSpender 1000000 \
--contract 0xUSDC \
--address 0xYourWalletAddress \
--chain BASE
```
The CLI waits for the transaction to reach a terminal state and returns the
result:
```json theme={null}
{
"data": {
"id": "abc-123-...",
"state": "CONFIRMED",
"blockchain": "BASE",
"txHash": "0xabc...",
"operation": "CONTRACT_EXECUTION",
"contractAddress": "0xUSDC",
"abiFunctionSignature": "approve(address,uint256)"
}
}
```
If the transaction fails, the CLI prints the reason. Verify the contract
address, function signature, and parameters, then retry.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including `--amount` to send native token value with
the call.
# How to: Fund an Agent Wallet
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/fund
Add USDC to your agent wallet using a wallet transfer or fiat onramp.
Add USDC to your agent wallet before sending transfers or paying for services.
This is a prerequisite for
[transferring USDC](/agent-stack/agent-wallets/wallet-operations/transfer),
[bridging USDC](/agent-stack/agent-wallets/wallet-operations/bridge),
[paying for services](/agent-stack/agent-nanopayments/operations/pay-for-service),
and other operations that spend USDC.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate)
your agent wallet.
## Steps
Run `circle wallet fund` with your wallet address, blockchain, amount, and
method (crypto or fiat). Replace `0xYourWalletAddress` with your wallet
address.
```bash theme={null}
circle wallet fund --address 0xYourWalletAddress \
--chain BASE --amount 10 --method crypto
```
The CLI prints a terminal QR code and an
[EIP-681](https://eips.ethereum.org/EIPS/eip-681) deposit URI. Scan
the QR code with your mobile wallet to send the funds. Pass
`--export ` to save a PNG instead, or `--open` to render the QR
code in a browser tab.
```bash theme={null}
circle wallet fund --address 0xYourWalletAddress \
--chain BASE --amount 10 --method fiat
```
Your browser opens to the onramp provider where you can purchase USDC
with a card or bank transfer. Pass `--no-open` to print the URL
instead of opening the browser.
To use testnet, replace `BASE` with a testnet blockchain (for example,
`ARC-TESTNET`) and omit `--method` and `--amount`. Testnet wallets are
auto-funded with 20 USDC from the Circle faucet. See [supported
blockchains](/agent-stack/agent-wallets/supported-blockchains) for the full
list.
Check your balance to confirm the deposit:
```bash theme={null}
circle wallet balance --address 0xYourWalletAddress --chain BASE
```
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including `--token` to fund with ETH or native tokens.
# How to: Sign a Message
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/sign
Sign a plain text message or EIP-712 typed data with your agent wallet.
Sign messages or EIP-712 typed data with your agent wallet. Signing is commonly
used to prove wallet ownership or authorize offchain actions.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* [Authenticated](/agent-stack/agent-wallets/wallet-operations/authenticate)
your agent wallet.
## Sign a message
Run `circle wallet sign` with your message and wallet details. Each command
returns a signature (`0xabcdef1234...`):
```bash theme={null}
# Sign a plain text message
circle wallet sign message "hello world" --address 0xYourWalletAddress --chain BASE
# Sign a hex-encoded message
circle wallet sign message "0xdeadbeef" --hex --address 0xYourWalletAddress --chain BASE
# Sign EIP-712 typed data (EVM only)
circle wallet sign typed-data \
'{"types":{...},"primaryType":"Mail","domain":{...},"message":{...}}' \
--address 0xYourWalletAddress \
--chain BASE
```
Typed data signing is supported on EVM blockchains only.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options.
# How to: Swap Tokens
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/swap
Swap one token for another from your agent wallet.
Swap tokens from your agent wallet using `circle wallet swap`. Optionally get a
price quote first to verify the expected output before committing funds.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Steps
Run `circle wallet swap` with `--quote` to see the estimated output without
executing the swap:
```bash theme={null}
circle wallet swap EURC 10 USDC --chain BASE --quote
```
```json theme={null}
{
"data": {
"message": "Quote: 10 EURC → ~9.95 USDC (min 0.000001) on BASE",
"sellToken": "EURC",
"sellAmount": "10",
"buyToken": "USDC",
"chain": "BASE",
"estimatedOutput": "9.95",
"stopLimit": "0.000001"
}
}
```
Run `circle wallet swap` with your wallet address and a ``
stop-limit. If the estimated output falls below this value onchain, the swap
fails instead of settling at an unfavorable rate:
```bash theme={null}
circle wallet swap EURC 10 USDC 9.9 --address 0xYourWalletAddress --chain BASE
```
```json theme={null}
{
"data": {
"message": "Swap complete: 10 EURC → min 9.9 USDC on BASE",
"sellToken": "EURC",
"sellAmount": "10",
"buyToken": "USDC",
"buyMin": "9.9",
"chain": "BASE"
}
}
```
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including `--slippage-bps` to set maximum slippage.
# How to: Transfer USDC
Source: https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/transfer
Send USDC from your agent wallet to another address on a supported blockchain.
The transfer confirms onchain before the command returns, so no manual polling
is required.
## Prerequisites
Before you begin, ensure you have:
* Installed [Node.js v20.18.2 or later](https://nodejs.org/).
* Installed Circle CLI:
```bash theme={null}
npm install -g @circle-fin/cli
```
* Completed the
[Agent Wallets quickstart](/agent-stack/agent-wallets/quickstart)
(authenticates and funds your wallet).
## Send USDC
Run `circle wallet transfer` with the recipient address, amount, and your wallet
details:
```bash theme={null}
circle wallet transfer 0xRecipient --amount 1.0 --address 0xYourWalletAddress --chain BASE
```
The command returns the result once the transaction reaches a terminal state:
```json theme={null}
{
"data": {
"id": "abc-123-...",
"state": "CONFIRMED",
"blockchain": "BASE",
"txHash": "0xabc...",
"sourceAddress": "0xYourWalletAddress",
"destinationAddress": "0xRecipient",
"amounts": ["1"],
"operation": "TRANSFER"
}
}
```
If the transfer fails, the command prints the reason. Check your wallet balance
and retry.
See the [CLI Command Reference](/agent-stack/circle-cli/command-reference) for
full syntax and options, including `--token` to transfer tokens other than USDC.
# Circle CLI
Source: https://developers.circle.com/agent-stack/circle-cli
A command-line tool to access Circle's product suite, including agent wallets, x402-compatible payments, and crosschain transfers.
Circle CLI gives developers and AI agents a unified interface for onchain
operations. Create and manage wallets, transfer, swap, and bridge USDC across
blockchains, execute smart contracts, and discover or pay for services. All
through a single command interface, without integrating multiple APIs and SDKs.
Use Circle CLI to access:
* **[Agent Wallets](/agent-stack/agent-wallets)**: Wallets with email OTP
authentication, full onchain capabilities, and customizable policy
enforcement.
* **Local wallets**: Self-custodial wallets imported from a private key or
mnemonic, stored using the
[Open Wallet Standard](https://github.com/open-wallet-standard/core).
* **Circle products**: [CCTP](/cctp) for USDC bridging and [Gateway](/gateway)
for [x402 nanopayments](/gateway/nanopayments).
## Installation
Install Circle CLI from [npm](https://www.npmjs.com/) (requires
[Node.js v20.18.2 or later](https://nodejs.org/)):
```bash theme={null}
npm install -g @circle-fin/cli
```
Verify the installation:
```bash theme={null}
circle --version
```
Once installed, you can run any
[Circle CLI command](/agent-stack/circle-cli/command-reference).
## Get started
Create an agent wallet and fund it with USDC.
All Circle CLI commands, options, and examples.
# CLI Command Reference
Source: https://developers.circle.com/agent-stack/circle-cli/command-reference
Complete command reference for Circle's agent command-line tool, organized by resource.
Circle CLI follows a `circle [options]` pattern. All commands
support `--help` for inline documentation.
## Global options
| Option | Description |
| :-------------------------- | :------------------------------------- |
| `--output json\|text\|yaml` | Set output format. Defaults to `text`. |
| `-q, --quiet` | Minimal output, suitable for piping. |
| `-h, --help` | Show help for any command. |
| `-v, --version` | Print the Circle CLI version and exit. |
***
## Wallet commands
Manage your [agent wallet](/agent-stack/agent-wallets).
### `circle wallet login`
Authenticate using email OTP to create or access an agent wallet session.
**Syntax**
```bash theme={null}
circle wallet login [options]
circle wallet login --init
circle wallet login --request --otp
```
**Options**
| Option | Description |
| :--------------- | :------------------------------------------------------------------------------------------------------------------- |
| `--type` | Wallet type. Defaults to `agent`. |
| `--testnet` | Authenticate against testnet. Sessions are stored separately from mainnet. |
| `--init` | Two-step login for scripts and AI agents. Sends the OTP and returns a request ID. Pair with `--request`. |
| `--request ` | Complete a `--init` login. Combine with `--otp `. |
| `--otp ` | One-time password for the request ID. Required with `--request`. Codes are alphanumeric (for example, `B1X-123456`). |
**Examples**
```bash theme={null}
# Interactive login (mainnet)
circle wallet login you@example.com
# Two-step login for scripts and AI agents
circle wallet login you@example.com --init
circle wallet login --request --otp B1X-123456
```
Request IDs from `--init` are stored at `~/.circle/login-requests/.json`,
expire after 10 minutes, and are deleted after a successful `--request`.
***
### `circle wallet logout`
Clear stored credentials for the current session.
**Syntax**
```bash theme={null}
circle wallet logout [options]
```
**Options**
| Option | Description |
| :------- | :------------------------------------------------------------------- |
| `--type` | Clear credentials for a specific wallet type (for example, `agent`). |
**Example**
```bash theme={null}
circle wallet logout --type agent
```
***
### `circle wallet status`
Show the current authentication status and session details.
**Syntax**
```bash theme={null}
circle wallet status [options]
```
**Options**
| Option | Description |
| :------- | :------------------------------------------------------------- |
| `--type` | Show status for a specific wallet type (for example, `agent`). |
**Example**
```bash theme={null}
circle wallet status --type agent
```
***
### `circle wallet create`
Create an additional wallet, separate from the wallets provisioned during login.
Each user can have at most 5 agent wallets.
**Syntax**
```bash theme={null}
circle wallet create [options]
```
**Options**
| Option | Description |
| :------------------ | :-------------------------------------------------------- |
| `--type` | Wallet type: `agent` (default) or `local`. |
| `--testnet` | Create a testnet wallet. Omit for mainnet. |
| `--idempotency-key` | Unique key to prevent duplicate wallet creation on retry. |
**Example**
```bash theme={null}
circle wallet create --type agent --testnet
```
***
### `circle wallet list`
List wallets associated with your account.
**Syntax**
```bash theme={null}
circle wallet list --chain [options]
```
**Options**
| Option | Description |
| :-------- | :----------------------------------------- |
| `--chain` | Blockchain to list wallets on. |
| `--type` | Filter by wallet type: `agent` or `local`. |
**Example**
```bash theme={null}
circle wallet list --chain ARC-TESTNET --type agent
```
***
### `circle wallet limit`
Show spending policy limits for an agent wallet. Mainnet only.
**Syntax**
```bash theme={null}
circle wallet limit --address --chain
```
**Options**
| Option | Description |
| :---------- | :------------------------------------------------ |
| `--address` | Agent wallet address. |
| `--chain` | Mainnet blockchain. Testnet chains not supported. |
**Examples**
```bash theme={null}
circle wallet limit --address 0x... --chain BASE
circle wallet limit --address 0x... --chain BASE --output json
```
***
### `circle wallet limit set`
Set a custom spending policy for an agent wallet. Requires a second email OTP to
confirm the change. Mainnet only.
Use `--rule-type` to choose the kind of policy:
* `transfer-limit` (default): cap how much USDC the wallet can transfer per
transaction or over a rolling time window. Set with `--per-tx`, `--daily`,
`--weekly`, `--monthly`.
* `recipient-allowlist` / `recipient-blocklist`: allow or block transfers to
specific recipient addresses. Set with `--targets`.
* `contract-allowlist` / `contract-blocklist`: allow or block contract
interactions with specific addresses. Set with `--targets`.
**Syntax**
```bash theme={null}
# Transfer limit (default)
circle wallet limit set --address --chain \
--policy-type \
[--per-tx ] [--daily ] [--weekly ] [--monthly ] \
[options]
# Allowlist or blocklist
circle wallet limit set --address --chain \
--policy-type \
--rule-type \
--targets "[0xAddr1,0xAddr2]" \
[options]
```
**Options**
| Option | Description |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--address` | Agent wallet address. |
| `--chain` | Mainnet blockchain. Testnet blockchains are not supported. |
| `--policy-type` | Policy category: `stablecoin` for transfer-based rules, `contract` for contract-based rules. Auto-set when `--rule-type` is a contract rule. |
| `--rule-type` | Rule shape: `transfer-limit` (default), `recipient-allowlist`, `recipient-blocklist`, `contract-allowlist`, or `contract-blocklist`. |
| `--per-tx` | Per-transaction spending cap. Used with `--rule-type transfer-limit`. |
| `--daily` | Daily rolling-window cap. Used with `--rule-type transfer-limit`. |
| `--weekly` | Weekly rolling-window cap. Used with `--rule-type transfer-limit`. |
| `--monthly` | Monthly rolling-window cap. Used with `--rule-type transfer-limit`. |
| `--targets` | Bracketed comma-separated list of EVM addresses (for example, `"[0xA,0xB]"`). Required for allowlist and blocklist rule types. Validated client-side. |
| `--email` | Email address for the confirmation OTP. Defaults to the agent session email. |
Transfer limits must satisfy: per-transaction ≤ daily ≤ weekly ≤ monthly.
**Examples**
```bash theme={null}
# Transfer limits
circle wallet limit set \
--address 0x... \
--chain BASE \
--policy-type stablecoin \
--per-tx 100 \
--daily 500 \
--weekly 2000 \
--monthly 5000
# Recipient blocklist
circle wallet limit set \
--address 0x... \
--chain BASE \
--policy-type stablecoin \
--rule-type recipient-blocklist \
--targets "[0xBAD1,0xBAD2]"
```
***
### `circle wallet limit reset`
Reset all custom spending policies for an agent wallet back to defaults.
Requires a second email OTP to confirm. Mainnet only.
**Syntax**
```bash theme={null}
circle wallet limit reset --address --chain [options]
```
**Options**
| Option | Description |
| :------------ | :------------------------------------------------ |
| `--address` | Agent wallet address. |
| `--chain` | Mainnet blockchain. Testnet chains not supported. |
| `--yes`, `-y` | Skip the confirmation prompt. |
**Example**
```bash theme={null}
circle wallet limit reset --address 0x... --chain BASE --yes
```
***
### `circle wallet limit budget`
Show remaining spending budgets for an agent wallet. Displays per-transaction
limits and rolling-window remaining amounts (daily, weekly, monthly). Budgets
are EVM-wide and not blockchain-specific. Mainnet only.
**Syntax**
```bash theme={null}
circle wallet limit budget --address
```
**Options**
| Option | Description |
| :---------- | :-------------------- |
| `--address` | Agent wallet address. |
**Example**
```bash theme={null}
circle wallet limit budget --address 0x...
```
***
### `circle wallet balance`
Show the token balance for a wallet address on a given blockchain.
**Syntax**
```bash theme={null}
circle wallet balance --address --chain [options]
```
**Options**
| Option | Description |
| :---------- | :------------------------------------------------------------------------------ |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
| `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. |
**Example**
```bash theme={null}
circle wallet balance --address 0x... --chain BASE
```
***
### `circle wallet fund`
Add funds to a wallet by transfer from another wallet (crypto), through a fiat
onramp, or from the testnet faucet.
**Syntax**
```bash theme={null}
circle wallet fund --address --chain --amount --method [options]
```
**Options**
| Option | Description |
| :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `--address` | Wallet address to fund. |
| `--chain` | Blockchain. |
| `--amount` | Amount to fund. Ignored on testnet. |
| `--token` | Token: `usdc` (default), `eth`, `native`. |
| `--method` | Funding method: `crypto` or `fiat`. Required on mainnet. Omit on testnet, where wallets auto-fund from the Circle faucet. |
| `--export ` | With `--method crypto`, write a PNG QR code into `` instead of printing it to the terminal. |
| `--open` | Open the result in your browser. With `--method fiat`, opens the onramp provider. With `--method crypto`, opens an HTML page with the QR code. |
| `--no-open` | Print the onramp URL without opening it. Used with `--method fiat` only. |
**Examples**
```bash theme={null}
# Fund with crypto from another wallet
circle wallet fund --address 0x... --chain BASE --amount 10 --method crypto
# Fund with fiat through the onramp provider
circle wallet fund --address 0x... --chain BASE --amount 10 --method fiat
# Testnet auto-funded from faucet (omit --method and --amount)
circle wallet fund --address 0x... --chain ARC-TESTNET
```
***
### `circle wallet transfer`
Transfer tokens from a wallet to another address.
**Syntax**
```bash theme={null}
circle wallet transfer --amount --address --chain [options]
```
**Arguments**
| Argument | Description |
| :------------ | :----------------- |
| `` | Recipient address. |
**Options**
| Option | Description |
| :----------- | :------------------------------------------------------------------------------ |
| `--amount` | Amount to transfer. |
| `--address` | Source wallet address. |
| `--chain` | Blockchain. |
| `--token` | Token contract address. Omit to use USDC. |
| `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. |
| `--estimate` | Show estimated fees without submitting the transfer. |
**Example**
```bash theme={null}
circle wallet transfer 0xRecipient --amount 5.0 --address 0x... --chain ARC-TESTNET
```
***
### `circle wallet swap`
Swap one token for another. Requires an agent wallet. Arc Testnet is the only
testnet supported.
**Syntax**
```bash theme={null}
circle wallet swap [] --address --chain [options]
```
**Arguments**
| Argument | Description |
| :-------------- | :--------------------------------------------------------------------- |
| `` | Token to sell. Use a symbol (for example, `EURC`) or contract address. |
| `` | Amount to sell. |
| `` | Token to buy. Use a symbol (for example, `USDC`) or contract address. |
| `[]` | Minimum acceptable output (stop-limit). Omit when using `--quote`. |
**Options**
| Option | Description |
| :------------------ | :---------------------------------------------------------------------------------------------- |
| `--address` | Agent wallet address. Optional when using `--quote`. |
| `--chain` | Blockchain. |
| `--quote` | Get a price quote without executing the swap. Does not require wallet ownership or `buyAmount`. |
| `--slippage-bps` | Maximum slippage in basis points (for example, `50` = 0.5%). |
| `--idempotency-key` | Unique key to prevent duplicate swaps on retry. |
**Examples**
```bash theme={null}
circle wallet swap EURC 100 USDC 99.5 --address 0x... --chain ARC-TESTNET
circle wallet swap EURC 100 USDC --chain ARC-TESTNET --quote
```
***
### `circle wallet sign message`
Sign a plain text or hex-encoded message with your wallet.
**Syntax**
```bash theme={null}
circle wallet sign message --address --chain [options]
```
**Arguments**
| Argument | Description |
| :---------- | :----------------------------------- |
| `` | Message to sign (plain text or hex). |
**Options**
| Option | Description |
| :---------- | :--------------------------------------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
| `--hex` | Message is hex-encoded (must start with `0x`). |
**Example**
```bash theme={null}
circle wallet sign message "hello world" --address 0x... --chain ARC-TESTNET
```
***
### `circle wallet sign typed-data`
Sign EIP-712 typed data with your wallet.
**Syntax**
```bash theme={null}
circle wallet sign typed-data --address --chain
```
**Arguments**
| Argument | Description |
| :------- | :----------------------------------- |
| `` | EIP-712 typed data as a JSON string. |
**Options**
| Option | Description |
| :---------- | :-------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
**Example**
```bash theme={null}
circle wallet sign typed-data '{"types":{...},"primaryType":"Mail","domain":{...},"message":{...}}' \
--address 0x... \
--chain ARC-TESTNET
```
***
### `circle wallet execute`
Execute a smart contract write function from a wallet.
**Syntax**
```bash theme={null}
circle wallet execute [...] \
--contract \
--address \
--chain \
[options]
```
**Arguments**
| Argument | Description |
| :----------------------- | :---------------------------------------------------------------- |
| `` | ABI function signature (for example, `approve(address,uint256)`). |
| `[...]` | ABI parameters, space-separated. |
**Options**
| Option | Description |
| :----------- | :------------------------------------------------------------------------------ |
| `--contract` | Contract address. |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
| `--amount` | Native token value to send with the call. Defaults to `0`. |
| `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. |
| `--estimate` | Show estimated fees without submitting the transaction. |
**Example**
```bash theme={null}
circle wallet execute "approve(address,uint256)" 0xSpender 1000000 \
--contract 0xUSDC \
--address 0x... \
--chain ARC-TESTNET
```
***
### `circle wallet import`
Import a local wallet from a private key or mnemonic phrase. Stored using the
Open Wallet Standard at `~/.ows/wallets/`.
Local wallets bypass Circle's compliance and safety controls. Spending
policies, OFAC screening, and audit logging only apply to agent wallets.
**Syntax**
```bash theme={null}
circle wallet import [--private-key | --mnemonic]
```
**Options**
| Option | Description |
| :-------------- | :----------------------------------------------------------------------- |
| `--private-key` | Import from a private key. You'll be prompted to enter the key securely. |
| `--mnemonic` | Import from a mnemonic phrase. You'll be prompted to enter it securely. |
Do not pass your private key or mnemonic as a command-line argument or
environment variable in plain text. Enter it at the prompt or use a secrets
manager.
**Example**
```bash theme={null}
circle wallet import my-wallet --private-key
```
***
## Services commands
Discover and pay for [x402](/gateway/nanopayments/concepts/x402)-compatible API
services.
### `circle services search`
Search for available services by keyword. Omit the query to list all services.
**Syntax**
```bash theme={null}
circle services search [] [options]
```
**Arguments**
| Argument | Description |
| :---------- | :----------------------------------------------------------- |
| `[]` | Optional search keyword or phrase. Omit to list all results. |
**Options**
| Option | Description |
| :----------- | :----------------------------------------------------------------------------- |
| `--category` | Filter by category (for example, `FINANCIAL_ANALYSIS`, `WEB_SEARCH_RESEARCH`). |
| `--type` | Filter by service type. |
| `--limit` | Maximum number of results to return. Defaults to `50`. |
| `--offset` | Number of results to skip, for pagination. Defaults to `0`. |
**Example**
```bash theme={null}
circle services search "weather" --category WEB_SEARCH_RESEARCH --limit 20
```
***
### `circle services inspect`
Inspect the payment requirements for a service URL. The CLI auto-detects the
HTTP method from the service's discovery metadata and auto-generates a minimal
request body from its input schema. Override either with the flags below.
**Syntax**
```bash theme={null}
circle services inspect [options]
```
**Arguments**
| Argument | Description |
| :------- | :---------------------- |
| `` | Service URL to inspect. |
**Options**
| Option | Description |
| :--------------- | :----------------------------------------------------------------------- |
| `--method`, `-X` | HTTP method override: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. |
| `--data`, `-d` | Request body as a JSON string. Overrides the auto-generated body. |
| `--header`, `-H` | Custom request header as `Key: Value`. Repeat the flag to send multiple. |
**Example**
```bash theme={null}
circle services inspect https://api.example.com/weather -X POST -d '{"city":"SF"}'
```
***
### `circle services pay`
Pay for a service using your agent wallet.
**Syntax**
```bash theme={null}
circle services pay --address --chain [options]
```
**Arguments**
| Argument | Description |
| :------- | :---------------------- |
| `` | Service URL to pay for. |
**Options**
| Option | Description |
| :-------------------- | :----------------------------------------------------------------------- |
| `--address` | Agent wallet address. |
| `--chain` | Blockchain to pay from. |
| `--max-amount ` | Refuse to pay more than this amount in USDC (for example, `0.01`). |
| `--method`, `-X` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. Defaults to `GET`. |
| `--data`, `-d` | Request body as a JSON string. |
| `--header`, `-H` | Custom request header as `Key: Value`. |
| `--estimate` | Show payment requirements without submitting payment. |
| `--quiet`, `-q` | Print response body only (useful for piping). |
| `--timeout ` | Per-step timeout in seconds. Defaults to `30`. |
**Example**
```bash theme={null}
circle services pay https://api.example.com/weather --address 0x... --chain BASE
```
Failed payments write debug logs to `~/.circle-cli/payments/`. Check the most
recent file for the request, response, and stage where the failure occurred.
***
## Bridge commands
Bridge USDC across blockchains using [CCTP](/cctp).
### `circle bridge transfer`
Bridge USDC from one blockchain to another.
**Syntax**
```bash theme={null}
circle bridge transfer [] --amount --address --chain
```
**Arguments**
| Argument | Description |
| :-------------- | :-------------------------------------------------------------------------- |
| `` | Destination blockchain (for example, `ARB`, `ETH`). |
| `[]` | Recipient address on the destination. Defaults to the value of `--address`. |
**Options**
| Option | Description |
| :------------------ | :-------------------------------------------------- |
| `--amount` | USDC amount the recipient will receive. |
| `--address` | Sender wallet address. |
| `--chain` | Source blockchain. |
| `--rpc-url` | RPC endpoint override for the source blockchain. |
| `--idempotency-key` | Unique key to prevent duplicate transfers on retry. |
| `--quiet`, `-q` | Print transaction hash only (useful for piping). |
**Example**
```bash theme={null}
circle bridge transfer ARB-SEPOLIA --amount 10.0 --address 0x... --chain ARC-TESTNET
```
***
### `circle bridge status`
Check the status of a bridge transfer by transaction hash.
**Syntax**
```bash theme={null}
circle bridge status --chain
```
**Arguments**
| Argument | Description |
| :--------- | :---------------------------------------- |
| `` | Transaction hash of the burn transaction. |
**Options**
| Option | Description |
| :-------- | :----------------- |
| `--chain` | Source blockchain. |
**Example**
```bash theme={null}
circle bridge status 0xabc... --chain ARC-TESTNET
```
***
### `circle bridge get-fee`
Get the estimated fee for bridging from a given blockchain.
**Syntax**
```bash theme={null}
circle bridge get-fee --chain
```
**Arguments**
| Argument | Description |
| :------- | :-------------------------------------------------- |
| `` | Destination blockchain (for example, `ARB`, `ETH`). |
**Options**
| Option | Description |
| :-------- | :----------------- |
| `--chain` | Source blockchain. |
**Example**
```bash theme={null}
circle bridge get-fee ETH --chain ARC-TESTNET
```
***
## Gateway commands
Interact with [Circle Gateway](/gateway).
### `circle gateway balance`
Show your Gateway balance for nanopayments.
**Syntax**
```bash theme={null}
circle gateway balance --address --chain [options]
```
**Options**
| Option | Description |
| :---------- | :------------------------------------------------------------------------------------------------------------------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain where the wallet lives. Any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. |
| `--all` | Show all blockchains including those with zero balances. |
| `--rpc-url` | RPC endpoint override. Required for local wallets without a configured default. |
**Examples**
```bash theme={null}
circle gateway balance --address 0x... --chain BASE
circle gateway balance --address 0x... --chain BASE --all
```
***
### `circle gateway deposit`
Deposit USDC into Circle Gateway for nanopayments. Minimum deposit is `0.5`
USDC.
**Syntax**
```bash theme={null}
circle gateway deposit --amount --address --chain --method [options]
```
**Options**
| Option | Description |
| :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--amount` | Amount of USDC to deposit. |
| `--address` | Wallet address. |
| `--chain` | Source blockchain. With `--method direct`, any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. With `--method eco`, only `BASE` and `BASE-SEPOLIA`. |
| `--method` | Deposit method: `eco` or `direct`. Eco deposits always settle on Polygon (`MATIC` mainnet, `MATIC-AMOY` testnet) regardless of source. |
| `--timeout` | Transaction poll timeout in seconds. Defaults to `120`. |
Eco is a third-party fast-deposit service that Circle does not operate or
audit. Review [Eco's
docs](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits)
and test the flow before using it in production.
**Example**
```bash theme={null}
circle gateway deposit --amount 5 --address 0x... --chain BASE --method eco
```
***
### `circle gateway withdraw`
Withdraw USDC from Circle Gateway back to a wallet on the same blockchain.
**Syntax**
```bash theme={null}
circle gateway withdraw --amount --address --chain [options]
```
**Options**
| Option | Description |
| :------------ | :--------------------------------------------------------------------------------------------------------- |
| `--amount` | Amount of USDC to withdraw. |
| `--address` | Source wallet address (the Gateway depositor). |
| `--chain` | Source blockchain. Any [Gateway-supported blockchain](/gateway/references/supported-blockchains) is valid. |
| `--recipient` | Destination address to receive USDC. Defaults to `--address`. |
| `--timeout` | Mint transaction poll timeout in seconds. Defaults to `120`. Agent wallets only. |
Withdrawals are same-chain only. The withdrawn USDC is minted on `--chain`.
Agent wallets must be Smart Contract Accounts (SCAs). Pass the SCA address as
`--address`. JSON output includes `transferId`, `estimatedFee`, and
`chargedFee`.
**Examples**
```bash theme={null}
# Withdraw to your own wallet
circle gateway withdraw --amount 0.1 --address 0x... --chain BASE
# Withdraw to a different recipient
circle gateway withdraw --amount 5 --address 0x... --chain BASE --recipient 0xOTHER
```
***
## Blockchain commands
Query supported blockchain information.
### `circle blockchain list`
List all blockchains supported by Circle CLI.
**Syntax**
```bash theme={null}
circle blockchain list
```
***
### `circle blockchain config`
Show or update the RPC URL for a blockchain.
**Syntax**
```bash theme={null}
circle blockchain config --chain [options]
```
**Options**
| Option | Description |
| :---------- | :------------------------------------------------------------- |
| `--chain` | Blockchain to configure. |
| `--rpc-url` | Set a custom RPC URL override for this blockchain. |
| `--default` | Reset to the default RPC URL. Cannot be used with `--rpc-url`. |
**Examples**
```bash theme={null}
circle blockchain config --chain ARC-TESTNET --output json
circle blockchain config --chain ARC-TESTNET --rpc-url https://my-node.example.com
circle blockchain config --chain ARC-TESTNET --default
```
***
## Transaction commands
Manage pending and submitted transactions.
### `circle transaction list`
List transaction history for a wallet.
**Syntax**
```bash theme={null}
circle transaction list --address --chain [options]
```
**Options**
| Option | Description |
| :--------------- | :---------------------------------------------------------------------------------------------------------------------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
| `--operation` | Filter by operation: `transfer` or `execute`. |
| `--state` | Filter by state: `initiated`, `queued`, `sent`, `confirmed`, `complete`, `failed`, `cancelled`, `denied`, `cleared`, `stuck`. |
| `--tx-type` | Filter by direction: `inbound` or `outbound`. |
| `--lowest-nonce` | Return only the lowest-nonce pending transaction. Ignores other filters. |
| `--cursor` | Pagination token: return transactions after this ID. |
| `--limit` | Maximum number of transactions to return. Defaults to `50`. |
**Examples**
```bash theme={null}
circle transaction list --address 0x... --chain ARC-TESTNET
circle transaction list --address 0x... --chain ARC-TESTNET --operation transfer --state confirmed
```
***
### `circle transaction cancel`
Cancel a pending transaction.
**Syntax**
```bash theme={null}
circle transaction cancel --address --chain
```
**Arguments**
| Argument | Description |
| :------- | :-------------- |
| `` | Transaction ID. |
**Options**
| Option | Description |
| :---------- | :-------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
**Example**
```bash theme={null}
circle transaction cancel abc-123 --address 0x... --chain ARC-TESTNET
```
***
### `circle transaction accelerate`
Accelerate a pending transaction by increasing the gas fee.
**Syntax**
```bash theme={null}
circle transaction accelerate --address --chain
```
**Arguments**
| Argument | Description |
| :------- | :-------------- |
| `` | Transaction ID. |
**Options**
| Option | Description |
| :---------- | :-------------- |
| `--address` | Wallet address. |
| `--chain` | Blockchain. |
**Example**
```bash theme={null}
circle transaction accelerate abc-123 --address 0x... --chain ARC-TESTNET
```
***
## Contract commands
Interact with onchain contracts.
### `circle contract address`
Show Circle contract addresses, optionally filtered by category and blockchain.
**Syntax**
```bash theme={null}
circle contract address [category] [--chain ]
```
**Arguments**
| Argument | Description |
| :----------- | :----------------------------------------------------------------------- |
| `[category]` | Contract category to filter by (for example, `usdc`, `cctp`, `gateway`). |
**Options**
| Option | Description |
| :-------- | :-------------------- |
| `--chain` | Filter by blockchain. |
**Examples**
```bash theme={null}
circle contract address usdc --chain ARC-TESTNET
circle contract address cctp --output json
```
***
### `circle contract query`
Execute a read-only contract call.
**Syntax**
```bash theme={null}
circle contract query [abiParameters...] --contract --chain
```
**Arguments**
| Argument | Description |
| :----------------------- | :---------------------------------------------------------- |
| `` | ABI function signature (for example, `balanceOf(address)`). |
| `[abiParameters...]` | ABI parameters, space-separated (for example, `0x1234...`). |
**Options**
| Option | Description |
| :----------- | :------------------- |
| `--contract` | Contract address. |
| `--chain` | Blockchain to query. |
**Examples**
```bash theme={null}
circle contract query "balanceOf(address)" 0xWALLET --contract 0xUSDC --chain ARC-TESTNET
circle contract query "totalSupply()" --contract 0xUSDC --chain ARC-TESTNET --output json
```
***
## Skill commands
Discover and install skills from the
[`circlefin/skills`](https://github.com/circlefin/skills) catalog.
The `--tool` option specifies your agent framework. Common values:
`claude-code`, `cursor`, `codex`.
### `circle skill list`
List all available skills from the catalog.
**Syntax**
```bash theme={null}
circle skill list [--output json]
```
**Options**
| Option | Description |
| :-------------- | :---------------------- |
| `--output json` | Return results as JSON. |
***
### `circle skill info`
Show details and full content for a specific skill.
**Syntax**
```bash theme={null}
circle skill info --name
```
**Options**
| Option | Description |
| :------- | :---------------------------- |
| `--name` | Name of the skill to inspect. |
**Example**
```bash theme={null}
circle skill info --name
```
***
### `circle skill install`
Install a skill into your agent framework.
**Syntax**
```bash theme={null}
circle skill install --tool [--name ]
```
**Options**
| Option | Description |
| :------- | :------------------------------------------------------------------------------------------ |
| `--tool` | Agent framework to install into. Use multiple `--tool` options for more than one framework. |
| `--name` | Skill name to install. Omit to install all available skills. |
**Examples**
```bash theme={null}
circle skill install --tool claude-code --name
circle skill install --tool cursor --tool codex --name
```
***
### `circle skill update`
Update installed skills for an agent framework.
**Syntax**
```bash theme={null}
circle skill update --tool
```
**Options**
| Option | Description |
| :------- | :------------------------------------ |
| `--tool` | Agent framework to update skills for. |
***
## Terms commands
Inspect, accept, or reset your local Circle CLI Terms of Use acceptance record.
The first time you run any command, Circle CLI prompts you to accept the Terms
of Use and Privacy Policy. Acceptance is stored locally and reused on subsequent
runs. To handle Terms acceptance non-interactively in scripts and AI agents, use
`circle terms accept` or set `CIRCLE_ACCEPT_TERMS=1` in the environment.
### `circle terms`
Show the current acceptance status and the canonical Terms of Use and Privacy
Policy URLs. Default verb is `show`.
**Syntax**
```bash theme={null}
circle terms [show] [options]
```
**Options**
| Option | Description |
| :------- | :----------------------------------------------------------------------------------------------------------------------- |
| `--init` | Return Terms info (version, URLs, notice text) for an agent to present before calling `accept`. Implies `--output json`. |
**Examples**
```bash theme={null}
circle terms
circle terms --output json
circle terms show --init --output json
```
**JSON output**
```json theme={null}
{
"accepted": true,
"currentVersion": "1.0.0",
"termsOfUseUrl": "https://www.circle.com/legal/circle-cli-terms-of-use",
"privacyPolicyUrl": "https://www.circle.com/legal/privacy-policy",
"acceptance": {
"version": "1.0.0",
"acceptedAt": "2026-05-07T12:34:56Z"
}
}
```
***
### `circle terms accept`
Explicitly accept the Terms of Use and Privacy Policy. Use this in scripts and
AI agent workflows after the user provides explicit consent.
**Syntax**
```bash theme={null}
circle terms accept [--output json]
```
**Example**
```bash theme={null}
circle terms accept --output json
```
***
### `circle terms reset`
Clear the local acceptance record. The next command run prompts you to accept
the Terms again.
**Syntax**
```bash theme={null}
circle terms reset
```
***
## Telemetry commands
Manage CLI telemetry preferences. Telemetry collects privacy-preserving usage
data to help improve Circle CLI.
### `circle telemetry status`
Show the current telemetry preference.
**Syntax**
```bash theme={null}
circle telemetry status [options]
```
**Example**
```bash theme={null}
circle telemetry status
```
***
### `circle telemetry enable`
Enable telemetry for future commands.
**Syntax**
```bash theme={null}
circle telemetry enable
```
***
### `circle telemetry disable`
Disable telemetry for future commands.
**Syntax**
```bash theme={null}
circle telemetry disable
```
# Start building with AI
Source: https://developers.circle.com/ai/chatbot
Collaborate with AI to generate code or learn how to build with Circle. You can
generate code for any of these Circle products: Circle Wallets, Contracts, CCTP,
Gateway. For AI-assisted development in your IDE, you can also use
[Circle's MCP Server](/ai/mcp) or install [AI skills](/ai/skills) for
specialized guidance.
# Use Circle's MCP Server in Your IDE
Source: https://developers.circle.com/ai/mcp
Integrate Circle's MCP server to let your LLM or AI-assisted IDE generate and
fix code for crypto apps using Circle's offerings, including Wallets, Contracts,
CCTP, and Gateway.
CLI commands
```shell Claude Code icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/claude-logo.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=2711bd5dc795308c62570a011248ef2d" theme={null}
claude mcp add --transport http circle https://api.circle.com/v1/codegen/mcp --scope user
```
```shell Codex icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/open-ai-logo-1.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=6a603cbffbbef93cfa078c899e8f19e8" theme={null}
codex mcp add circle --url https://api.circle.com/v1/codegen/mcp
```
## Quick setup
Add Circle's MCP server to your client with the following details:
* **Server Name**: `circle`
* **Server URL**: `https://api.circle.com/v1/codegen/mcp`
## Installation with your IDE
Select your MCP client to view detailed setup instructions:
> **Note:** If you are using a client that is not listed here, you can still use
> the Circle MCP server by manually adding the server URL to your client's
> configuration.
Download and install [Cursor](https://cursor.com) if you haven't already.
[1-Click Set Up](cursor://anysphere.cursor-deeplink/mcp/install?name=circle\&config=eyJ1cmwiOiJodHRwczovL2FwaS5jaXJjbGUuY29tL3YxL2NvZGVnZW4vbWNwIn0%3D)
Manual steps:
1. Open a project in Cursor and navigate to **Cursor Settings**.
2. In the settings menu, go to the **MCP** section.
3. Click **New MCP Server**. This will open your `mcp.json` configuration file.
4. Add the following configuration:
```json theme={null}
{
"mcpServers": {
"circle": {
"url": "https://api.circle.com/v1/codegen/mcp"
}
}
}
```
5. Return to the MCP settings page and enable the server using the toggle switch
next to `circle`. Start generating code for Circle Wallets, Contracts, CCTP,
and Gateway.
For more information on how to use MCP with Cursor, see the
[Cursor MCP documentation](https://cursor.com/docs/context/mcp).
Download and install
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) if you
haven't already.
1. Using the Claude Code command line add the MCP server with the following
command:
```shell theme={null}
claude mcp add --transport http circle https://api.circle.com/v1/codegen/mcp --scope user
```
2. Verify the server is added by running the following command:
```shell theme={null}
claude mcp get circle
```
3. Start generating code for Circle Wallets, Contracts, CCTP, and Gateway.
For more information on how to use MCP with Claude Code, see the
[Claude Code MCP documentation](https://docs.claude.com/en/docs/claude-code/mcp).
Download and install
[Windsurf](https://docs.windsurf.com/windsurf/getting-started) if you haven't
already.
1. Open the `~/.codeium/windsurf/mcp_config.json` file and add the following:
```json theme={null}
{
"mcpServers": {
"circle": {
"url": "https://api.circle.com/v1/codegen/mcp"
}
}
}
```
2. Enable the Circle MCP server in your MCP settings. Start generating code for
Circle Wallets, Contracts, CCTP, and Gateway.
For more information on how to use MCP with Windsurf, see the
[Windsurf MCP documentation](https://docs.windsurf.com/windsurf/cascade/mcp).
Download and install [Kiro](https://kiro.dev) if you haven't already.
1. Open Kiro and go to **Preferences/Settings** → search for "MCP" → enable MCP
support.
2. Create or open the MCP config file:
* **Workspace-level**: `./.kiro/settings/mcp.json` (recommended for
project-specific)
* **User-level**: `~/.kiro/settings/mcp.json`
3. Add a server configuration entry for Circle:
```json theme={null}
{
"mcpServers": {
"circle": {
"command": "npx",
"args": ["-y", "@circle/mcp-server"],
"env": {
"CIRCLE_BASE_URL": "https://api.circle.com/v1/codegen/mcp"
},
"disabled": false
}
}
}
```
4. Save and then restart Kiro (or reload the MCP server list) so the new server
appears in the MCP tab.
5. In Kiro's side panel → **MCP Servers** tab → you should see "circle-mcp"
listed. Start generating code for Circle Wallets, Contracts, CCTP, and
Gateway.
Kiro's general MCP setup: [Kiro Docs - MCP](https://kiro.dev/docs/mcp/).
```
```
# AI Skills for Building with Circle
Source: https://developers.circle.com/ai/skills
Use Circle's open source AI skills to accelerate development with AI-assisted
IDEs. Skills provide specialized knowledge for building with Circle's products,
including wallets, crosschain transfers, and smart contracts.
Skills are available in the
[circlefin/skills](https://github.com/circlefin/skills) repository.
## Installation
Use the following commands to install Circle Skills with the command-line.
```shell Claude Code icon="https://mintcdn.com/circle-167b8d39/5X_H2DLmIxVDSWCs/images/claude-logo.png?fit=max&auto=format&n=5X_H2DLmIxVDSWCs&q=85&s=2711bd5dc795308c62570a011248ef2d" theme={null}
/plugin marketplace add circlefin/skills
/plugin install circle-skills@circle
```
```shell Vercel Skills CLI icon="https://mintcdn.com/circle-167b8d39/kF52uen9TY9sspam/images/vercel_logo.png?fit=max&auto=format&n=kF52uen9TY9sspam&q=85&s=20d2a3e59acc98f14fe41854458cfcd5" theme={null}
npx skills add circlefin/skills
```
## Available skills
The following skills are available to help you build with Circle's products.
### `bridge-stablecoin`
Build apps that bridge USDC between chains using Circle's
[Cross-Chain Transfer Protocol (CCTP)](/cctp). Includes UX patterns, progress
tracking, destination chain linking, and
[Bridge Kit](https://docs.arc.io/app-kit/bridge) SDK implementation patterns for
EVM and Solana chains.
### `use-arc`
Build on Arc, Circle's blockchain where USDC is the native gas token. Covers
chain configuration, [smart contract](/contracts) deployment with Foundry or
Hardhat, frontend integration with viem/wagmi, and bridging USDC to Arc via
[CCTP](/cctp).
### `use-circle-wallets`
Choose the right [Circle wallet](/wallets) type for your application. Compares
[developer-controlled](/wallets/dev-controlled),
[user-controlled](/wallets/user-controlled), and [modular](/wallets/modular)
(passkey) wallets across custody model, key management, account types, and
blockchain support.
### `use-developer-controlled-wallets`
[Developer-controlled wallets](/wallets/dev-controlled) where developers manage
wallet creation, storage, and key management. Use for custodial or operational
flows like payouts, treasury movements, subscriptions, and automation.
### `use-gateway`
Implement [Circle Gateway](/gateway) unified balance for crosschain USDC
transfers. Supports instant transfers (under 500ms) across EVM and Solana chains
with deposit, balance query, and transfer workflows.
### `use-modular-wallets`
Build [modular wallets](/wallets/modular) with passkey authentication, gasless
transactions, and modular architecture. Supports ERC-4337 account abstraction
and ERC-6900 modular framework.
### `use-smart-contract-platform`
Deploy, import, interact with, and monitor smart contracts using
[Circle's Smart Contract Platform](/contracts). Supports bytecode deployment,
template contracts (ERC-20/721/1155), ABI-based read/write calls, and event
monitoring.
### `use-user-controlled-wallets`
Build embedded [user-controlled wallets](/wallets/user-controlled) where users
control their own assets. Supports Web2-like login experiences (Google,
Facebook, Apple, email OTP, PIN) without seed phrases.
# API Reference
Source: https://developers.circle.com/api-reference
Overview of Circle's available APIs and endpoints.
Circle provides a suite of REST APIs for building financial applications on
blockchain infrastructure. Whether you're creating wallets, deploying smart
contracts, moving USDC across blockchains, or building institutional payment
flows, there is an API tailored to your use case.
## Before you begin
Many Circle APIs require an API key to authenticate requests. Permissionless
products like CCTP and Gateway are open and require no API key.
Learn about API keys, client keys, and kit keys, and how to authenticate
requests to Circle's platform
Use idempotency keys to safely retry API calls without creating duplicate
operations
## Available APIs
Create and manage developer-controlled and user-controlled wallets, execute
transactions, and sign messages across EVM, Solana, and other supported
blockchains
Deploy and interact with smart contracts using Circle's managed
infrastructure, including event monitoring and contract templates
Fetch attestations and support native USDC transfers across blockchains
using Cross-Chain Transfer Protocol
Access and manage a unified USDC balance across multiple blockchains with
instant transfers in under 500 ms
Manage USDC and EURC balances, process crypto deposits and payouts, execute
cross-currency trades, and manage reserves
Route and settle stablecoin payments across Circle's network with support
for quotes, payments, and transactions
Request quotes and execute institutional FX trades between USDC and EURC
with onchain settlement on Arc
Deposit USDC into xReserve, retrieve attestations, and manage withdrawals
for USDC-backed stablecoins
# Get an attestation
Source: https://developers.circle.com/api-reference/cctp/all/get-attestation
/openapi/cctp.yaml get /v1/attestations/{messageHash}
Retrieves the signed attestation for a USDC burn event on the source chain.
# Get USDC transfer fees
Source: https://developers.circle.com/api-reference/cctp/all/get-burn-usdc-fees
/openapi/cctp.yaml get /v2/burn/USDC/fees/{sourceDomainId}/{destDomainId}
Retrieves the applicable fees for a USDC transfer between the specified source and destination domains. The fee is returned in basis points (1 = 0.01%).
# Get USDC Fast Transfer allowance
Source: https://developers.circle.com/api-reference/cctp/all/get-fast-burn-usdc-allowance
/openapi/cctp.yaml get /v2/fastBurn/USDC/allowance
Retrieves the available USDC Fast Transfer allowance remaining.
# Get a list of messages
Source: https://developers.circle.com/api-reference/cctp/all/get-messages
/openapi/cctp.yaml get /v1/messages/{sourceDomainId}/{transactionHash}
Retrieves message and attestation details for CCTP V1 messages.
# Get messages and attestations
Source: https://developers.circle.com/api-reference/cctp/all/get-messages-v2
/openapi/cctp.yaml get /v2/messages/{sourceDomainId}
Retrieves messages and attestations for a given transaction hash or nonce. Each message for a given transaction hash is ordered by ascending log index.
# List attestation public keys
Source: https://developers.circle.com/api-reference/cctp/all/get-public-keys
/openapi/cctp.yaml get /v1/publicKeys
Retrieves a list of the currently active public keys for verifying attestation signatures.
# Get public keys
Source: https://developers.circle.com/api-reference/cctp/all/get-public-keys-v2
/openapi/cctp.yaml get /v2/publicKeys
Returns the public keys for validating attestations across all supported versions of CCTP.
# Re-attest a pre-finality message
Source: https://developers.circle.com/api-reference/cctp/all/reattest-message
/openapi/cctp.yaml post /v2/reattest/{nonce}
The re-attestation flow allows the relayer to obtain a higher level of finality than was originally requested on the source chain, while still being forced to pay the fee since allowance was reserved. This flow resolves the case where a sender specifies a finality threshold lower than the destination chain recipient requires.
# Create a CUBIX bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-cubix-account
/openapi/account.yaml post /v1/businessAccount/banks/cubix
# Create a deposit address
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-deposit-address
/openapi/account.yaml post /v1/businessAccount/wallets/addresses/deposit
Generates a new blockchain address for a wallet for a given currency/chain pair. Circle may reuse addresses on blockchains that support reuse. For example, if you're requesting two addresses for depositing USD and ETH, both on Ethereum, you may see the same Ethereum address returned. Depositing cryptocurrency to a generated address will credit the associated wallet with the value of the deposit. Note: Circle Mint Singapore customers must verify all transfer recipients using the UI in the Circle Console, as transfers from unverified addresses will be held in `pending` status.
# Create a payout
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-payout
/openapi/account.yaml post /v1/businessAccount/payouts
Create a redemption (offramp) payout. This payout converts a digital asset to fiat currency.
# Create a PIX bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-pix-account
/openapi/account.yaml post /v1/businessAccount/banks/pix
# Create a recipient address
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-recipient-address
/openapi/account.yaml post /v1/businessAccount/wallets/addresses/recipient
Stores an external blockchain address. Once added, the recipient address must be verified to ensure that you know and trust each new address.
**For France customers:**
Circle Mint France customers must verify all transfer recipients using the UI in the Circle Console, as transfers from unverified addresses will be held in pending status. Please see Help Center articles below for details:
- [Circle Mint France Travel Rule](https://help.circle.com/s/article/Circle-Mint-France-Travel-Rule)
- [Circle Mint France wallet verification](https://help.circle.com/s/article/Circle-Mint-France-wallet-verification)
# Create a transfer
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-transfer
/openapi/account.yaml post /v1/businessAccount/transfers
A transfer can be made from an existing business account to a blockchain location.
# Create a wire bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/create-business-wire-account
/openapi/account.yaml post /v1/businessAccount/banks/wires
# Create a mock Wire payment
Source: https://developers.circle.com/api-reference/circle-mint/account/create-mock-wire-payment
/openapi/account.yaml post /v1/mocks/payments/wire
In the sandbox environment, initiate a mock wire payment that mimics the behavior of funds sent through the bank (wire) account linked to master wallet.
# Delete a recipient address
Source: https://developers.circle.com/api-reference/circle-mint/account/delete-business-recipient-address
/openapi/account.yaml delete /v1/businessAccount/wallets/addresses/recipient/{id}
Deletes an external blockchain address. The recipient address must be in an 'active' or 'pending' state in order to be deleted successfully.
# Get associated accounts
Source: https://developers.circle.com/api-reference/circle-mint/account/get-associated-accounts
/openapi/account.yaml get /v1/businessAccount/associatedAccounts
Returns a list of sibling CMAs that are associated with the current account under unified credentials. These accounts can be used as destinations for cross-entity transfers.
# Get a CUBIX bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-cubix-account
/openapi/account.yaml get /v1/businessAccount/banks/cubix/{id}
# Get CUBIX instructions
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-cubix-account-instructions
/openapi/account.yaml get /v1/businessAccount/banks/cubix/{id}/instructions
Get the CUBIX transfer instructions into the Circle bank account given your fiat account id.
# List all deposit addresses
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-deposit-address
/openapi/account.yaml get /v1/businessAccount/wallets/addresses/deposit
Returns a list of deposit addresses for a given wallet.
# Get a deposit by ID
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-deposit-by-id
/openapi/account.yaml get /v1/businessAccount/deposits/{id}
Returns a deposit by ID.
# Get a payout
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-payout
/openapi/account.yaml get /v1/businessAccount/payouts/{id}
# Get a PIX bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-pix-account
/openapi/account.yaml get /v1/businessAccount/banks/pix/{id}
# Get PIX instructions
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-pix-account-instructions
/openapi/account.yaml get /v1/businessAccount/banks/pix/{id}/instructions
Get the PIX transfer instructions into the Circle bank account given your bank account id.
# Get a transfer
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-transfer
/openapi/account.yaml get /v1/businessAccount/transfers/{id}
# Get a wire bank account
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-wire-account
/openapi/account.yaml get /v1/businessAccount/banks/wires/{id}
# Get wire instructions
Source: https://developers.circle.com/api-reference/circle-mint/account/get-business-wire-account-instructions
/openapi/account.yaml get /v1/businessAccount/banks/wires/{id}/instructions
Get the wire transfer instructions into the Circle bank account given your bank account ID.
# Get PIX routing info
Source: https://developers.circle.com/api-reference/circle-mint/account/get-pix-routing-info
/openapi/account.yaml get /v1/businessAccount/banks/pix/{fiatAccountId}/routingInfo
Retrieves available settlement banks and current routing configuration for a PIX fiat account.
# Get report by ID
Source: https://developers.circle.com/api-reference/circle-mint/account/get-report-by-id
/openapi/account.yaml get /v1/reports/{id}
Returns the current metadata for a report, including a fresh pre-signed `downloadUrl` when the report is `ready`.
Use this endpoint to check the status of a `pending` report or to obtain a new download URL after the previous one has expired.
# Download report content
Source: https://developers.circle.com/api-reference/circle-mint/account/get-report-content
/openapi/account.yaml get /v1/reports/{id}/content
Streams the raw report content as a file download, an alternative to following `downloadUrl` from the JSON response.
Returns `409` if the report is not yet `ready`.
# Get wire routing info
Source: https://developers.circle.com/api-reference/circle-mint/account/get-wire-routing-info
/openapi/account.yaml get /v1/businessAccount/banks/wires/{fiatAccountId}/routingInfo
Retrieves available settlement banks and current routing configuration for a wire fiat account.
# List all balances
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-balances
/openapi/account.yaml get /v1/businessAccount/balances
Retrieves the balance of funds that are available for use.
# List all CUBIX bank accounts.
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-cubix-accounts
/openapi/account.yaml get /v1/businessAccount/banks/cubix
# List all deposits
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-deposits
/openapi/account.yaml get /v1/businessAccount/deposits
Searches for deposits sent to your business account. If the date parameters are omitted, returns the most recent deposits. This endpoint returns up to 50 deposits in descending chronological order or pageSize, if provided.
# List all payouts
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-payouts
/openapi/account.yaml get /v1/businessAccount/payouts
Lists all payouts for your account.
Note that this endpoint does not return the tracking reference number for the payouts in the response. If you need that information you must get each payout individually by ID.
# List all PIX bank accounts.
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-pix-accounts
/openapi/account.yaml get /v1/businessAccount/banks/pix
# List all recipient addresses
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-recipient-addresses
/openapi/account.yaml get /v1/businessAccount/wallets/addresses/recipient
Returns a list of recipient addresses that have each been verified and are eligible for transfers. Any recipient addresses pending administrator verification are not included in the response.
# List all transfers
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-transfers
/openapi/account.yaml get /v1/businessAccount/transfers
Searches for transfers from your business account. If the date parameters are omitted, returns the most recent transfers. This endpoint returns up to 50 transfers in descending chronological order or pageSize, if provided.
# List all wire bank accounts
Source: https://developers.circle.com/api-reference/circle-mint/account/list-business-wire-accounts
/openapi/account.yaml get /v1/businessAccount/banks/wires
# List daily burn fee calculations
Source: https://developers.circle.com/api-reference/circle-mint/account/list-net-burn-fee-daily-calculations
/openapi/account.yaml get /v1/fees/redemption/dailyReports
Returns daily burn fee calculations. This endpoint returns up to 50 daily fee calculations in descending chronological order or `pageSize`, if provided.
# Request a report
Source: https://developers.circle.com/api-reference/circle-mint/account/request-report
/openapi/account.yaml post /v1/reports
Submits a report generation request. `reportType` specifies the type of report and required fields.
If ready, returns `200` with a pre-signed `downloadUrl`. If not, returns `202` with status `pending`. Poll `GET /v1/reports/{id}` for status, or download using `GET /v1/reports/{id}/content` when ready.
Requests are idempotent: the same entity and request parameters always produce the same report ID. Retries return the existing report.
# Update PIX routing preferences
Source: https://developers.circle.com/api-reference/circle-mint/account/update-pix-routing-preferences
/openapi/account.yaml put /v1/businessAccount/banks/pix/{fiatAccountId}/routingPreferences
Creates or updates the settlement bank routing preferences for a PIX fiat account.
At least one of `inboundBankLabel` or `outboundBankLabel` must be provided in the request.
Note: This endpoint will reject updates if the account has active Express routes configured. Accounts with Express routes must update preferences through the Circle Mint UI.
# Update wire routing preferences
Source: https://developers.circle.com/api-reference/circle-mint/account/update-wire-routing-preferences
/openapi/account.yaml put /v1/businessAccount/banks/wires/{fiatAccountId}/routingPreferences
Creates or updates the settlement bank routing preferences for a wire fiat account.
At least one of `inboundBankLabel` or `outboundBankLabel` must be provided in the request.
Note: This endpoint will reject updates if the account has active Express routes configured. Accounts with Express routes must update preferences through the Circle Mint UI.
# Cancel reserved funds
Source: https://developers.circle.com/api-reference/circle-mint/credit/cancel-credit-transfer-reserve
/openapi/credit.yaml put /v1/credit/transfers/{id}/cancelReserve
Cancels a `funds_reserved` transfer and releases the reserved amount back to available credit.
The transfer must be in `funds_reserved` status for this operation.
**Note:** This endpoint is only available for Settlement Advance products.
# Initiate a crypto repayment
Source: https://developers.circle.com/api-reference/circle-mint/credit/create-credit-crypto-repayment
/openapi/credit.yaml post /v1/credit/cryptoRepayment
Initiates a crypto repayment for a credit transfer.
The requested amount is capped at the outstanding balance. If the minimum of the requested amount and outstanding balance is zero, the request will be rejected with HTTP 400.
**Note:** This endpoint is only available for Line of Credit products. Crypto repayment is not supported for Settlement Advance products.
# Create a credit transfer
Source: https://developers.circle.com/api-reference/circle-mint/credit/create-credit-transfer
/openapi/credit.yaml post /v1/credit/transfers
Requests a new credit transfer (drawdown) from the credit line. Disbursement is asynchronous.
**Note:** This endpoint is only available for Line of Credit products. Settlement Advance products must use the reserve funds flow.
# Create a mock wire repayment
Source: https://developers.circle.com/api-reference/circle-mint/credit/create-mock-wire-repayment
/openapi/credit.yaml post /v1/credit/mocks/repayments
In the sandbox environment, initiate a mock wire repayment that simulates an incoming wire payment to repay an outstanding credit transfer. The fiat account will be automatically linked as a repayment account if not already linked.
# Get a credit fee
Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-fee
/openapi/credit.yaml get /v1/credit/fees/{id}
Returns detailed information about a specific fee.
# Get credit line details
Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-line
/openapi/credit.yaml get /v1/credit
Provides overall credit line details, including status, available limit, and outstanding balance.
# Get a credit repayment
Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-repayment
/openapi/credit.yaml get /v1/credit/repayments/{id}
Returns detailed information about a specific repayment.
# Get a credit transfer
Source: https://developers.circle.com/api-reference/circle-mint/credit/get-credit-transfer
/openapi/credit.yaml get /v1/credit/transfers/{id}
Returns detailed information about a specific credit transfer.
Fields `outstanding`, `fees`, `dueDate`, and `disbursedDate` are only present once the transfer reaches `disbursed`, `paid`, or `past_due` status.
# Get repayment account details
Source: https://developers.circle.com/api-reference/circle-mint/credit/get-repayment-account-detail
/openapi/credit.yaml get /v1/credit/repaymentAccounts/{fiatAccountId}
Returns repayment account details and wire instructions for making a credit repayment using the specified fiat account.
# List all credit fees
Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-fees
/openapi/credit.yaml get /v1/credit/fees
Returns a paginated list of all historical fees. Filterable by create date range, currency, and status.
# List all credit repayments
Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-repayments
/openapi/credit.yaml get /v1/credit/repayments
Returns a paginated list of all repayments (fiat and crypto). Filterable by create date range, transfer ID, type, and status.
# List all credit transfers
Source: https://developers.circle.com/api-reference/circle-mint/credit/list-credit-transfers
/openapi/credit.yaml get /v1/credit/transfers
Returns a paginated list of all credit transfers. Filterable by create date range and status. Transfer items in list responses do not include outstanding and fees properties.
# Request reserved funds
Source: https://developers.circle.com/api-reference/circle-mint/credit/request-credit-transfer-reserved-funds
/openapi/credit.yaml put /v1/credit/transfers/{id}/requestReservedFunds
Transitions a `funds_reserved` transfer to `requested` status by uploading evidence (wire proof). This initiates the manual approval process for Settlement Advance transfers.
The request must include an evidence file (wire proof) as multipart form data. Allowed file types are `application/pdf`, `image/jpeg`, and `image/png`.
**Note:** This endpoint is only available for Settlement Advance products.
# Reserve funds for a credit transfer
Source: https://developers.circle.com/api-reference/circle-mint/credit/reserve-credit-transfer-funds
/openapi/credit.yaml post /v1/credit/transfers/reserveFunds
Reserves funds for a Settlement Advance draw. This is the first mandatory step in the Settlement Advance transfer flow.
Reserved funds expire after 30 minutes if not progressed to `requested` status via the request reserved funds endpoint.
Only one transfer may be in `funds_reserved` status per credit line at a time.
**Note:** This endpoint is only available for Settlement Advance products.
# Create FX account
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-fx-account
/openapi/cross-currency.yaml put /v1/exchange/fxConfigs/accounts
Creates a currency trading account
# Create FX trade
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-fx-trade
/openapi/cross-currency.yaml post /v1/exchange/trades
Creates a cross-currency trade
# Create a mock PIX payment
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/create-mock-pix-payment
/openapi/cross-currency.yaml post /v1/mocks/payments/pix
Initiates a mock PIX payment in the sandbox environment that mimics the behavior of funds sent through the bank account linked to the main wallet.
# Get daily currency exchange limits
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-daily-fx-limits
/openapi/cross-currency.yaml get /v1/exchange/fxConfigs/dailyLimits
Returns daily currency exchange limits and usages.
# Get FX trade
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-fx-trade-id
/openapi/cross-currency.yaml get /v1/exchange/trades/{id}
Returns an FX trade by ID.
# Get all FX trades
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-fx-trades
/openapi/cross-currency.yaml get /v1/exchange/trades
Returns all cross-currency trades. You can include an optional `settlementId` query parameter to filter the trades to only a specific settlement.
# Get quote
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-quote
/openapi/cross-currency.yaml post /v1/exchange/quotes
Fetches an indicative exchange rate between two currencies. Either the from currency or to currency must be USD.
Note: The current market exchange rate will be applied when Circle receives the deposit.
# Get settlement
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlement-id
/openapi/cross-currency.yaml get /v1/exchange/trades/settlements/{id}
Returns a settlement by ID.
# Get settlement instructions
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlement-instructions
/openapi/cross-currency.yaml get /v1/exchange/trades/settlements/instructions/{currency}
Returns settlement instructions for a specific currency.
# Get all settlements
Source: https://developers.circle.com/api-reference/circle-mint/cross-currency/get-settlements
/openapi/cross-currency.yaml get /v1/exchange/trades/settlements
Returns all settlements.
# Create a notification subscription
Source: https://developers.circle.com/api-reference/circle-mint/general/create-subscription
/openapi/general.yaml post /v1/notifications/subscriptions
Subscribe to receiving notifications at a given endpoint. The endpoint should be able to handle AWS SNS subscription requests. For more details see https://docs.aws.amazon.com/mobile/sdkforxamarin/developerguide/sns-send-http.html. Note, the sandbox environment allows a maximum of 3 active subscriptions; otherwise, this is limited to 1 active subscription and subsequent create requests will be rejected with a Limit Exceeded error.
# Remove a notification subscription
Source: https://developers.circle.com/api-reference/circle-mint/general/delete-subscription
/openapi/general.yaml delete /v1/notifications/subscriptions/{id}
To remove a subscription, all its subscription requests' statuses must be either 'confirmed', 'deleted' or a combination of those. A subscription with at least one 'pending' subscription request cannot be removed.
# Get configuration info
Source: https://developers.circle.com/api-reference/circle-mint/general/get-account-config
/openapi/general.yaml get /v1/configuration
Retrieves general configuration information.
# List all stablecoins
Source: https://developers.circle.com/api-reference/circle-mint/general/list-stablecoins
/openapi/general.yaml get /v1/stablecoins
Retrieves total circulating supply for supported stablecoins across all chains. This endpoint is rate limited to one call per minute (based on IP).
# List all notification subscriptions
Source: https://developers.circle.com/api-reference/circle-mint/general/list-subscriptions
/openapi/general.yaml get /v1/notifications/subscriptions
Retrieve a list of existing notification subscriptions with details.
# Ping
Source: https://developers.circle.com/api-reference/circle-mint/general/ping
/openapi/general.yaml get /ping
Checks that the service is running.
# Create an external entity
Source: https://developers.circle.com/api-reference/circle-mint/institutional/create-external-entity
/openapi/institutional.yaml post /v1/externalEntities
Creates an external entity for the institutional account.
To access the Core API for Institutions, contact your Circle account representative.
# Get all external entities
Source: https://developers.circle.com/api-reference/circle-mint/institutional/get-all-external-entities
/openapi/institutional.yaml get /v1/externalEntities
Returns all external entities for the institutional account.
To access the Core API for Institutions, contact your Circle account representative.
Note that the `businessUniqueIdentifier` and `identifierIssuingCountryCode` must both be provided, or not at all. Only providing one will result in an error.
# Get an external entity by wallet ID
Source: https://developers.circle.com/api-reference/circle-mint/institutional/get-external-entity-by-wallet-id
/openapi/institutional.yaml get /v1/externalEntities/{walletId}
Returns an external entity by wallet ID.
To access the Core API for Institutions, contact your Circle account representative.
# Create a payment intent
Source: https://developers.circle.com/api-reference/circle-mint/payments/create-payment-intent
/openapi/payments.yaml post /v1/paymentIntents
Create a continuous (default) or transient payment intent. Continuous payment intents are created by default. To create a transient payment intent, the type field must be explicitly set to 'transient'.
# Expire a payment intent
Source: https://developers.circle.com/api-reference/circle-mint/payments/expire-payment-intent
/openapi/payments.yaml post /v1/paymentIntents/{id}/expire
# Get a payment
Source: https://developers.circle.com/api-reference/circle-mint/payments/get-payment
/openapi/payments.yaml get /v1/payments/{id}
# Get a payment intent
Source: https://developers.circle.com/api-reference/circle-mint/payments/get-payment-intent
/openapi/payments.yaml get /v1/paymentIntents/{id}
# List all payment intents
Source: https://developers.circle.com/api-reference/circle-mint/payments/list-payment-intents
/openapi/payments.yaml get /v1/paymentIntents
# List all payments
Source: https://developers.circle.com/api-reference/circle-mint/payments/list-payments
/openapi/payments.yaml get /v1/payments
# Refund a payment intent
Source: https://developers.circle.com/api-reference/circle-mint/payments/refund-payment-intent
/openapi/payments.yaml post /v1/paymentIntents/{id}/refund
# Create a recipient
Source: https://developers.circle.com/api-reference/circle-mint/payouts/create-address-book-recipient
/openapi/payouts.yaml post /v1/addressBook/recipients
Creates an address book recipient. Required fields depend on your Circle entity; use the request body options that match your integration.
Validation failures can return error codes in the `2024`–`2037` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing).
# Create a payout
Source: https://developers.circle.com/api-reference/circle-mint/payouts/create-payout
/openapi/payouts.yaml post /v1/payouts
Create a stablecoin payout.
The following table includes the supported pairs of `amount.currency` and `toAmount.currency` for stablecoin address book payouts:
| amount.currency | toAmount.currency |
| ---------------- | ----------------- |
| USD | USD |
| EUR | EUR |
Required fields depend on your Circle entity; use the request body options that match your integration.
For Singapore (CIRCLE_SG) entities, `purposeOfTransfer` is **required** and must use a payment reason code from [Crypto Payouts payment reason codes](https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes). Invalid or missing values can return error code `5020`.
# Delete a recipient
Source: https://developers.circle.com/api-reference/circle-mint/payouts/delete-address-book-recipient
/openapi/payouts.yaml delete /v1/addressBook/recipients/{id}
# Get a recipient
Source: https://developers.circle.com/api-reference/circle-mint/payouts/get-address-book-recipient
/openapi/payouts.yaml get /v1/addressBook/recipients/{id}
# Get a payout
Source: https://developers.circle.com/api-reference/circle-mint/payouts/get-payout
/openapi/payouts.yaml get /v1/payouts/{id}
# List all recipients
Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-address-book-recipients
/openapi/payouts.yaml get /v1/addressBook/recipients
# List VASPs
Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-address-book-vasps
/openapi/payouts.yaml get /v1/addressBook/vasps
Returns active Virtual Asset Service Providers (VASPs) available for the customer's jurisdiction. Use returned `id` values as `vaspId` in `ownership.custody` when creating a recipient and custody is `hosted`.
**Note:** This operation is supported only for Circle Singapore (SG) customers.
# List all payouts
Source: https://developers.circle.com/api-reference/circle-mint/payouts/list-payouts
/openapi/payouts.yaml get /v1/payouts
# Modify a recipient
Source: https://developers.circle.com/api-reference/circle-mint/payouts/modify-address-book-recipient
/openapi/payouts.yaml patch /v1/addressBook/recipients/{id}
Updates address book recipient metadata.
# Create daily custody balance report
Source: https://developers.circle.com/api-reference/circle-mint/reserve-management/report-daily-custody-balances
/openapi/reserve-management.yaml post /v2/reserveManagement/dailyCustodyBalances
Creates a daily custody balance report for USDC and EURC and sends it to Circle.
For `reportType=eea`, this endpoint represents the four reportable cells of EBA Template S 08.00 for one token and one reference date: CASP total token count and EUR value, plus the EU-client subset token count and EUR value.
MiCA / EBA S 08.00 mapping (`reportType=eea`):
- S 08.00 Row 0010 / Col 0010 maps to `additionalFields.totalBalance`
- S 08.00 Row 0010 / Col 0020 maps to `additionalFields.equivalentEuroTotalBalance`
- S 08.00 Row 0020 / Col 0010 maps to `localBalance`
- S 08.00 Row 0020 / Col 0020 maps to `additionalFields.equivalentEuroLocalBalance`
`localBalance` and `totalBalance` are token-unit counts. `equivalentEuroLocalBalance` and `equivalentEuroTotalBalance` are EUR values. EU clients are determined by habitual residence for natural persons and registered office for legal persons. USD/EUR FX conversion should be based on the ECB rate applicable for that date, as available on the ECB website.
Validation rules:
- `localBalance` must be less than or equal to `additionalFields.totalBalance`
- `equivalentEuroLocalBalance` must be less than or equal to `equivalentEuroTotalBalance`
- Only one submission per day per currency
- USDC and EURC require separate submissions
# Create a notification subscription
Source: https://developers.circle.com/api-reference/contracts/common/create-subscription
/openapi/configurations_2.yaml post /v2/notifications/subscriptions
Create a notification subscription by configuring an endpoint to receive notifications. For details, see the [Notification Flows](https://developers.circle.com/wallets/webhook-notification-flows) guide.
# Delete a notification subscription
Source: https://developers.circle.com/api-reference/contracts/common/delete-subscription
/openapi/configurations_2.yaml delete /v2/notifications/subscriptions/{id}
Delete an existing subscription.
# Get a notification signature public key
Source: https://developers.circle.com/api-reference/contracts/common/get-notification-signature
/openapi/configurations_2.yaml get /v2/notifications/publicKey/{id}
Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle.
In the headers of each webhook, you can find
1. `X-Circle-Signature`: a header containing the digital signature generated by Circle.
2. `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key.
# Retrieve a notification subscription
Source: https://developers.circle.com/api-reference/contracts/common/get-subscription
/openapi/configurations_2.yaml get /v2/notifications/subscriptions/{id}
Retrieve an existing notification subscription.
# Get all notification subscriptions
Source: https://developers.circle.com/api-reference/contracts/common/get-subscriptions
/openapi/configurations_2.yaml get /v2/notifications/subscriptions
Retrieve an array of existing notification subscriptions.
# Ping
Source: https://developers.circle.com/api-reference/contracts/common/ping
/openapi/configurations_2.yaml get /ping
Checks that the service is running.
# Update a notification subscription
Source: https://developers.circle.com/api-reference/contracts/common/update-subscription
/openapi/configurations_2.yaml patch /v2/notifications/subscriptions/{id}
Update subscription endpoint to receive notifications.
# Create Event Monitor
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/create-event-monitor
/openapi/smart-contract-platform.yaml post /v1/w3s/contracts/monitors
Create a new event monitor based on the provided blockchain, contract address, and event signature.
# Delete Event Monitor
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/delete-event-monitor
/openapi/smart-contract-platform.yaml delete /v1/w3s/contracts/monitors/{id}
Delete an existing event monitor given its ID.
# Deploy a contract
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract
/openapi/smart-contract-platform.yaml post /v1/w3s/contracts/deploy
Deploy a smart contract on a specified blockchain using the contract's ABI and bytecode. The deployment will originate from one of your Circle Wallets.
# Deploy a contract from a template
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/deploy-contract-template
/openapi/smart-contract-platform.yaml post /v1/w3s/templates/{id}/deploy
Deploy a smart contract using a template.
# Estimate a contract deployment
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/estimate-contract-deploy
/openapi/smart-contract-platform.yaml post /v1/w3s/contracts/deploy/estimateFee
Estimate the network fee for deploying a smart contract on a specified blockchain, given the contract bytecode.
# Estimate fee for a contract template deployment
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/estimate-contract-template-deploy
/openapi/smart-contract-platform.yaml post /v1/w3s/templates/{id}/deploy/estimateFee
Estimate the fee required to deploy contract by template.
# Get a contract
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-contract
/openapi/smart-contract-platform.yaml get /v1/w3s/contracts/{id}
Get a single contract that you've imported or deployed. Retrieved using the contracts ID as opposed to the on-chain address.
# Get Event Monitors
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-event-monitors
/openapi/smart-contract-platform.yaml get /v1/w3s/contracts/monitors
Fetch a list of event monitors, optionally filtered by blockchain, contract address, and event signature.
# Import a contract
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/import-contract
/openapi/smart-contract-platform.yaml post /v1/w3s/contracts/import
Add an existing smart contract to your library of contracts. It also can be done in the Developer Services Console.
# List contracts
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/list-contracts
/openapi/smart-contract-platform.yaml get /v1/w3s/contracts
Fetch a list of contracts that you've imported and/or deployed.
# Get Event Logs
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/list-event-logs
/openapi/smart-contract-platform.yaml get /v1/w3s/contracts/events
Fetch all event logs, optionally filtered by blockchain and contract address.
# Execute a query function on a contract
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/query-contract
/openapi/smart-contract-platform.yaml post /v1/w3s/contracts/query
Query the state of a contract by providing the address and blockchain.
# Update a contract
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/update-contract
/openapi/smart-contract-platform.yaml patch /v1/w3s/contracts/{id}
Update the off-chain properties, such as description, of a contract that you've imported or deployed. Updated using the contracts ID as opposed to the on-chain address.
# Update an Event Monitor
Source: https://developers.circle.com/api-reference/contracts/smart-contract-platform/update-event-monitor
/openapi/smart-contract-platform.yaml put /v1/w3s/contracts/monitors/{id}
Update an existing event monitor given its ID.
# Create a webhook subscription
Source: https://developers.circle.com/api-reference/cpn/common/create-subscription
/openapi/configurations.yaml post /v2/cpn/notifications/subscriptions
Create a webhook subscription by configuring an endpoint to receive notifications.
# Delete a notification subscription
Source: https://developers.circle.com/api-reference/cpn/common/delete-subscription
/openapi/configurations.yaml delete /v2/cpn/notifications/subscriptions/{id}
Delete an existing subscription.
# Get a notification signature public key
Source: https://developers.circle.com/api-reference/cpn/common/get-notification-signature
/openapi/configurations.yaml get /v2/cpn/notifications/publicKey/{id}
Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle.
In the headers of each webhook, you can find
- `X-Circle-Signature`: a header containing the digital signature generated by Circle.
- `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key.
# Get a notification subscription
Source: https://developers.circle.com/api-reference/cpn/common/get-subscription
/openapi/configurations.yaml get /v2/cpn/notifications/subscriptions/{id}
Returns an existing notification subscription.
# Get all webhook subscriptions
Source: https://developers.circle.com/api-reference/cpn/common/get-subscriptions
/openapi/configurations.yaml get /v2/cpn/notifications/subscriptions
Returns an array of existing webhook subscriptions.
# Ping
Source: https://developers.circle.com/api-reference/cpn/common/ping
/openapi/configurations.yaml get /ping
Checks that the service is running.
# Update a notification subscription
Source: https://developers.circle.com/api-reference/cpn/common/update-subscription
/openapi/configurations.yaml patch /v2/cpn/notifications/subscriptions/{id}
Update subscription endpoint to receive notifications.
# Accelerate a stuck transaction
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/accelerate-transaction
/openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions/accelerate
- Accelerate a transaction based on the payment ID. It should be used when a transaction associated with the payment is broadcasted but not confirmed for a long period of time (i.e 10 minutes). This is usually due to gas fees being too low and not picked up by any miner/validator.
- The /accelerate endpoint essentially creates another transaction with the same params as the broadcasted transaction. If multiple broadcasted transactions exist, it will use the newest created one. Afterwards, OFI can sign with a higher gas fee and submit via /submit endpoint to accelerate blockchain confirmation.
- Requirements for using this endpoint:
- No COMPLETED transaction exist for the payment (otherwise onchain transaction has completed)
- No CREATED transaction exist for the payment, otherwise OFI should sign that transaction and submit
- No PENDING transaction exist for the payment, otherwise OFI should wait for transaction to be broadcasted
- In another word, all existing transaction for the payment should either be FAILED (which is no longer effective) or BROADCASTED (which means they are stuck onchain and not confirmed)
# Create a payment
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-payment
/openapi/cpn-ofi.yaml post /v1/cpn/payments
Creates a payment by using the quote created previously and submitting recipient information (travel rule). The payment will remain valid if the onchain settlement occurs before settlementExpireDate.
# Create a quote
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-quotes
/openapi/cpn-ofi.yaml post /v1/cpn/quotes
Creates one or more quotes for the given source/destination parameters. Returns quotes sorted in the following order:
- Ascending of `sourceAmount` if your quote is based on `destinationAmount`.
- Descending of `destinationAmount` if your quote is based on `sourceAmount`.
# Create a support ticket
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-support-ticket
/openapi/cpn-ofi.yaml post /v1/cpn/supportTickets
Create transaction-related issues (for example, settlement delays, missing information, or refunds). These tickets are stored centrally in the CPN platform and routed to the appropriate party for resolution.
# Create a transaction
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-transaction
/openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions
Creates an unsigned onchain transaction for a specific payment.
# Create a transaction (V2)
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/create-transaction-v2
/openapi/cpn-ofi.yaml post /v2/cpn/payments/{paymentId}/transactions
Create a V2 transaction for signing
# Get a payment
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment
/openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}
Returns the PII fields needed to collect to make this payment (i.e. travel rule and beneficiary account data)
# Get payment configurations
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment-configurations-overview
/openapi/cpn-ofi.yaml get /v1/cpn/configurations/overview
Returns the overview of supported countries, currencies, payment methods, blockchains.
# Get payment requirements for a quote
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-payment-requirements
/openapi/cpn-ofi.yaml get /v1/cpn/payments/requirements
Retrieves the PII fields needed to collect to make this payment (travel rule and beneficiary account data).
# Get details of a quote
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-quote
/openapi/cpn-ofi.yaml get /v1/cpn/quotes/{quoteId}
Retrieve details of a specific quote (e.g., re-check expiration, fees).
# Get refund details
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-refund
/openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/refunds/{refundId}
Retrieves the full refund object associated with a specific payment. This can be used by OFIs to reconcile refund status and verify refund completion.
# Get details for an RFI
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-rfi
/openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/rfis/{rfiId}
Retrieve details of a specific RFI for a payment. If the BFI initiates an RFI after the payment is created, the OFI will be notified via webhook. This webhook will detail what specific information the OFI needs to send.
After receiving the webhook. The OFI is expected to encrypt the requested data and send it to the BFI using CPN's RFI submit endpoint. Failure to respond to an RFI will result in a failed payment. The OFI will receive webhooks with the decision based on the submitted information.
# Get a transaction
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-transaction
/openapi/cpn-ofi.yaml get /v1/cpn/payments/{paymentId}/transactions/{transactionId}
Retrieves a specific transaction by its ID for a given payment
# Get a transaction by ID (V2)
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/get-transaction-v2
/openapi/cpn-ofi.yaml get /v2/cpn/payments/{paymentId}/transactions/{transactionId}
Get a transaction by ID
# List payments
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/list-payments
/openapi/cpn-ofi.yaml get /v1/cpn/payments
Returns a list of all payments that fit the specified parameters.
# Get supported payment routes
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/list-routes
/openapi/cpn-ofi.yaml get /v1/cpn/configurations/routes
Returns a list of route details including trade limits. This information can determine
what corridors and parameters are valid for subsequent quote creation.
# Submit RFI data
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-rfi
/openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/rfis/{rfiId}/submit
Submit encrypted RFI data to complete an RFI request from the BFI.
# Submit a signed transaction for broadcast
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-transaction
/openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/transactions/{transactionId}/submit
Return the signed hex string of the transaction, Circle will validate the content and broadcast to the chain.
# Submit a signed transaction for broadcast (V2)
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/submit-transaction-v2
/openapi/cpn-ofi.yaml post /v2/cpn/payments/{paymentId}/transactions/{transactionId}/submit
Submit a signed V2 transaction for broadcast
# Upload RFI file
Source: https://developers.circle.com/api-reference/cpn/cpn-platform/upload-rfi-file
/openapi/cpn-ofi.yaml post /v1/cpn/payments/{paymentId}/rfis/{rfiId}/files
Upload encrypted RFI file.
# Create an account
Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/create-account
/openapi/accounts.yaml post /v1/accounts
Creates a new account. This account can be used to create accounts for Mint, or
intermediary accounts for a Managed Payments customer. For Managed Payments, the
account created represents a business entity that will settle stablecoin payments
via the customer.
Routing for this endpoint is body based. Requests that include the `businessPii`
field are routed to the Managed Payments intermediary account creation flow, while
requests that omit the `businessPii` field are routed to the standard account creation
flow.
For the standard account creation flow, If `clientEntityId` is not provided, the
account type will be `customer`. If `clientEntityId` is provided, the account type
will be `client`.
# Get an account
Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/get-account
/openapi/accounts.yaml get /v1/accounts/{accountId}
Retrieves a single account by `accountId`.
# List all accounts
Source: https://developers.circle.com/api-reference/cpn/managed-payments/accounts/list-accounts
/openapi/accounts.yaml get /v1/accounts
Retrieves the accounts available to the calling entity.
# Create a recipient
Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/create-address-book-recipient
/openapi/payouts.yaml post /v1/addressBook/recipients
Creates an address book recipient. Required fields depend on your Circle entity; use the request body options that match your integration.
Validation failures can return error codes in the `2024`–`2037` range (for example `2024` when `identity` is required but missing, `2025` when `ownership` is required but missing).
# Delete a recipient
Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/delete-address-book-recipient
/openapi/payouts.yaml delete /v1/addressBook/recipients/{id}
# Get a recipient
Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/get-address-book-recipient
/openapi/payouts.yaml get /v1/addressBook/recipients/{id}
# List all recipients
Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/list-address-book-recipients
/openapi/payouts.yaml get /v1/addressBook/recipients
# Modify a recipient
Source: https://developers.circle.com/api-reference/cpn/managed-payments/address-book/modify-address-book-recipient
/openapi/payouts.yaml patch /v1/addressBook/recipients/{id}
Updates address book recipient metadata.
# Borrow against line of credit
Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/create-managed-payments-credit-transfer
/openapi/managed-payments.yaml post /v1/managedPayments/credit/lines/{lineId}/transfers
Initiates a transfer (borrowing) against the line of credit.
# Get credit line details
Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-line
/openapi/managed-payments.yaml get /v1/managedPayments/credit/lines
Retrieves the details of the Managed Payments credit line.
# Get a credit transfer
Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-transfer
/openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/transfers/{transferId}
Returns detailed information about a specific credit transfer.
Fields `outstanding`, `dueDate`, and `disbursedDate` are present when the transfer status is `disbursed`, `paid`, or `past_due`. Field `paidDate` is present only when status is `paid`.
# Get repayment wire instructions
Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/get-managed-payments-credit-wire-instructions
/openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/wireInstructions
Fetches the necessary wire transfer instructions for repaying funds borrowed against the line of credit.
# List credit transfers
Source: https://developers.circle.com/api-reference/cpn/managed-payments/credit/list-managed-payments-credit-transfers
/openapi/managed-payments.yaml get /v1/managedPayments/credit/lines/{lineId}/transfers
Returns a list of credit transfers for the specified credit line. Filterable by status and date range.
# Get an account bank deposit
Source: https://developers.circle.com/api-reference/cpn/managed-payments/deposits/get-account-deposit
/openapi/accounts.yaml get /v1/accounts/deposits/{id}
Returns a bank deposit by ID.
# List all account bank deposits
Source: https://developers.circle.com/api-reference/cpn/managed-payments/deposits/list-account-deposits
/openapi/accounts.yaml get /v1/accounts/deposits
Searches for bank deposits sent to accounts. If the date parameters are omitted, returns the most recent deposits. This endpoint returns up to 50 deposits in descending chronological order or pageSize, if provided.
# Create a payment intent
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/create-payment-intent
/openapi/payments.yaml post /v1/paymentIntents
Create a continuous (default) or transient payment intent. Continuous payment intents are created by default. To create a transient payment intent, the type field must be explicitly set to 'transient'.
# Expire a payment intent
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/expire-payment-intent
/openapi/payments.yaml post /v1/paymentIntents/{id}/expire
# Get a payment intent
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/get-payment-intent
/openapi/payments.yaml get /v1/paymentIntents/{id}
# List all payment intents
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/list-payment-intents
/openapi/payments.yaml get /v1/paymentIntents
# Refund a payment intent
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payment-intents/refund-payment-intent
/openapi/payments.yaml post /v1/paymentIntents/{id}/refund
# Get a payment
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payments/get-payment
/openapi/payments.yaml get /v1/payments/{id}
# List all payments
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payments/list-payments
/openapi/payments.yaml get /v1/payments
# Create a payout
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/create-payout
/openapi/payouts.yaml post /v1/payouts
Create a stablecoin payout.
The following table includes the supported pairs of `amount.currency` and `toAmount.currency` for stablecoin address book payouts:
| amount.currency | toAmount.currency |
| ---------------- | ----------------- |
| USD | USD |
| EUR | EUR |
Required fields depend on your Circle entity; use the request body options that match your integration.
For Singapore (CIRCLE_SG) entities, `purposeOfTransfer` is **required** and must use a payment reason code from [Crypto Payouts payment reason codes](https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes). Invalid or missing values can return error code `5020`.
# Get a payout
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/get-payout
/openapi/payouts.yaml get /v1/payouts/{id}
# List all payouts
Source: https://developers.circle.com/api-reference/cpn/managed-payments/payouts/list-payouts
/openapi/payouts.yaml get /v1/payouts
# Create a wire bank account
Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/create-account-wire-account
/openapi/accounts.yaml post /v1/banks/wires
Create a bank account for wire transfers.
# Get a wire bank account
Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/get-account-wire-account
/openapi/accounts.yaml get /v1/banks/wires/{id}
Retrieves a specific wire bank account.
# Get wire instructions
Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/get-account-wire-account-instructions
/openapi/accounts.yaml get /v1/banks/wires/{id}/instructions
Retrieves wire transfer instructions for a specific bank account.
# List all wire bank accounts
Source: https://developers.circle.com/api-reference/cpn/managed-payments/wires/list-account-wire-accounts
/openapi/accounts.yaml get /v1/banks/wires
Retrieves a list of bank accounts for wire transfers.
# Create an account bank withdrawal
Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/create-account-withdrawal
/openapi/accounts.yaml post /v1/accounts/withdrawals
Create a bank withdrawal from an account. This converts a digital asset to fiat currency and sends it to the specified destination bank account.
# Get an account bank withdrawal
Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/get-account-withdrawal
/openapi/accounts.yaml get /v1/accounts/withdrawals/{id}
Retrieves a specific bank withdrawal.
# List all account bank withdrawals
Source: https://developers.circle.com/api-reference/cpn/managed-payments/withdrawals/list-account-withdrawals
/openapi/accounts.yaml get /v1/accounts/withdrawals
Lists all bank withdrawals for accounts.
# Create a webhook subscription
Source: https://developers.circle.com/api-reference/gateway/all/create-permissionless-subscription
/openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless
Create a permissionless webhook subscription by configuring an endpoint to receive event notifications. Specify the environment, wallet addresses to monitor, blockchain domains to watch, and event types to receive.
# Create a transfer attestation for transferring tokens
Source: https://developers.circle.com/api-reference/gateway/all/create-transfer-attestation
/openapi/gateway.yaml post /v1/transfer
Generates a transfer attestation and operator signature for transferring tokens between domains
# Delete a webhook subscription
Source: https://developers.circle.com/api-reference/gateway/all/delete-permissionless-subscription
/openapi/gateway.yaml delete /v2/notifications/subscriptions/permissionless/{id}
Delete an existing permissionless webhook subscription.
# Estimate fees and expiration block heights for a transfer
Source: https://developers.circle.com/api-reference/gateway/all/estimate-transfer
/openapi/gateway.yaml post /v1/estimate
Calculates the required fees and expiration block heights for a transfer without requiring signatures or executing the transaction.
# Get pending deposits for specified addresses
Source: https://developers.circle.com/api-reference/gateway/all/get-deposits
/openapi/gateway.yaml post /v1/deposits
Returns pending deposits for each specified depositor address across different domains where that address is valid.
# Get Gateway info for supported domains and tokens
Source: https://developers.circle.com/api-reference/gateway/all/get-gateway-info
/openapi/gateway.yaml get /v1/info
Provides information about the API and details of the supported domains and tokens.
# Get a notification signature public key
Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-notification-signature
/openapi/gateway.yaml get /v2/notifications/publicKey/{id}
Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle.
In the headers of each webhook, you can find
1. `X-Circle-Signature`: a header containing the digital signature generated by Circle.
2. `X-Circle-Key-Id`: a header containing the UUID. This value is used as the `ID` URL parameter to retrieve the relevant public key.
# Retrieve a webhook subscription
Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-subscription
/openapi/gateway.yaml get /v2/notifications/subscriptions/permissionless/{id}
Retrieve an existing permissionless webhook subscription.
# Get all webhook subscriptions
Source: https://developers.circle.com/api-reference/gateway/all/get-permissionless-subscriptions
/openapi/gateway.yaml get /v2/notifications/subscriptions/permissionless
Retrieve an array of existing permissionless webhook subscriptions.
# Get supported x402 payment kinds
Source: https://developers.circle.com/api-reference/gateway/all/get-supported-x402payment-kinds
/openapi/gateway.yaml get /v1/x402/supported
Returns the payment kinds supported by Circle Gateway for x402 batching.
Each kind includes the GatewayWallet contract address in
`extra.verifyingContract` which clients use for EIP-712 signing, and an
`extra.assets` array containing the supported tokens with their addresses,
symbols, and decimals.
# Get token balances for specified addresses
Source: https://developers.circle.com/api-reference/gateway/all/get-token-balances
/openapi/gateway.yaml post /v1/balances
Returns the current available balance of each specified address across different domains where that address is valid
# Get a transfer by ID
Source: https://developers.circle.com/api-reference/gateway/all/get-transfer-by-id
/openapi/gateway.yaml get /v1/transfer/{id}
Returns detailed information about a transfer.
# Get full TransferSpec by transferSpecHash
Source: https://developers.circle.com/api-reference/gateway/all/get-transfer-spec
/openapi/gateway.yaml get /v1/transferSpec/{transferSpecHash}
Retrieve the full TransferSpec for a given transferSpecHash.
# Get an x402 transfer by ID
Source: https://developers.circle.com/api-reference/gateway/all/get-x402transfer-by-id
/openapi/gateway.yaml get /v1/x402/transfers/{id}
Retrieves a single x402 transfer by its unique identifier.
# Search x402 transfers
Source: https://developers.circle.com/api-reference/gateway/all/search-x402transfers
/openapi/gateway.yaml get /v1/x402/transfers
Returns a paginated list of x402 transfers matching the given filters.
Supports cursor-based pagination via pageAfter / pageBefore.
# Send a test notification
Source: https://developers.circle.com/api-reference/gateway/all/send-permissionless-subscription-test-notification
/openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless/{id}/test
Send a test notification to the subscriber endpoint. The notification has notificationType "webhooks.test".
# Settle an x402 payment
Source: https://developers.circle.com/api-reference/gateway/all/settle-x402payment
/openapi/gateway.yaml post /v1/x402/settle
Settles an x402 payment by submitting the EIP-3009 authorization.
The authorization will be verified, the sender's balance locked, and
the transaction queued for batch processing.
# Submit an EIP-3009 authorization to be batched
Source: https://developers.circle.com/api-reference/gateway/all/submit-batch-authorization
/openapi/gateway.yaml post /v1/batch/submit
Submit a single-chain transfer authorization using EIP-3009 signature.
The authorization will be verified, the sender's balance locked, and
the transaction queued for batch processing.
# Test subscription connection
Source: https://developers.circle.com/api-reference/gateway/all/test-permissionless-subscription-connection
/openapi/gateway.yaml post /v2/notifications/subscriptions/permissionless/{id}/testConnection
Verify that the subscriber endpoint for the given subscription is reachable.
# Update a webhook subscription
Source: https://developers.circle.com/api-reference/gateway/all/update-permissionless-subscription
/openapi/gateway.yaml patch /v2/notifications/subscriptions/permissionless/{id}
Update a webhook subscription. Metadata fields can be updated independently. To update filters, provide `notificationTypes`, `addresses`, and `domains` together; those fields fully replace the existing filters.
# Verify an x402 payment payload
Source: https://developers.circle.com/api-reference/gateway/all/verify-x402payment
/openapi/gateway.yaml post /v1/x402/verify
Verifies that an x402 payment payload can be processed by running
all read-only validation checks (scheme, network, token, signature,
temporal constraints, address/amount matching). A valid result does
not guarantee settlement — balance and nonce checks only happen at
settle time.
# Idempotent Requests
Source: https://developers.circle.com/api-reference/idempotent-requests
Idempotency keys let you safely retry Circle API calls.
Circle APIs support
[idempotent requests](https://en.wikipedia.org/wiki/Idempotence), so making the
same request multiple times produces the same result. This lets you safely retry
API calls if something goes wrong.
## Idempotency keys
Certain endpoints require you to generate an idempotency key to identify the
request. For endpoints that require an idempotency key, each request must have a
unique key.
The server uses this key to identify a specific request. When a request is made
with the same idempotency key, the server returns the original response instead
of executing the operation again.
For endpoints that require it, the idempotency key must be in
[UUID version 4](https://en.wikipedia.org/wiki/Universally_unique_identifier)
format.
The following example demonstrates how to generate an idempotency key in
Node.js:
```typescript theme={null}
import crypto from "crypto";
function generateIdempotencyKey(): string {
return crypto.randomUUID();
}
const idempotencyKey: string = generateIdempotencyKey();
console.log(idempotencyKey); // e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479"
```
# API Keys
Source: https://developers.circle.com/api-reference/keys
Learn about the different types of API keys used to authenticate requests to Circle's platform.
Certain products use API keys to authenticate requests. Circle provides three
types of keys for different use cases: [API keys](#api-keys) for server-side
access, [client keys](#client-keys) for frontend applications, and
[kit keys](#kit-keys) for SDK integrations.
Permissionless products like CCTP and Gateway do not require an API key.
Authenticate server-side requests to Circle's RESTful APIs.
Authenticate client applications with domain or app binding. Required for
frontend SDKs.
Authenticate kit access with a single key that works on both testnet and
mainnet.
## API keys
An API key is a unique string used to authenticate and enable access to
privileged operations on Circle's APIs. It's required for any RESTful API
requests to Circle services. Without it, requests will fail.
### Keep your API keys safe
API keys allow access to sensitive operations, so you must secure them.
* **Avoid public exposure**: Never share API keys or include them in client-side
code, public repositories, or other public mediums.
* **Manage securely**: Use the Circle Console to generate and manage API keys.
When generating a key, copy it exactly as displayed.
Losing control of your API key can result in financial loss.
### API key authentication
Use the headers below to authenticate requests on testnet or mainnet.
#### Testnet authorization header example
```text theme={null}
authorization: Bearer TEST_API_KEY:ebb3ad72232624921abc4b162148bb84:019ef3358ef9cd6d08fc32csfe89a68d
```
#### Mainnet authorization header example
```text theme={null}
authorization: Bearer LIVE_API_KEY:ebb3ad72232624921abc4b162148bb84:019ef3358ef9cd6d08fc32csfe89a68d
```
### Test authentication
To verify your API key setup, use the following `curl` command to retrieve
wallets:
```bash theme={null}
curl --request GET \
--url https://api.circle.com/v1/w3s/wallets \
--header 'accept: application/json' \
--header 'authorization: Bearer '
```
A successful response looks like this:
```json theme={null}
{
"data": {
"wallets": []
}
}
```
An error response looks like this:
```json theme={null}
{
"code": 401,
"message": "Malformed authorization. Are the credentials properly encoded?"
}
```
***
## Client keys
A client key is a unique string used to authenticate and authorize API access
for apps using Circle's SDKs. A client key is linked to either a specific host
domain (websites), bundle ID (iOS), or package name (Android). This restricts
access to pre-configured apps.
A client key must be included in the headers of all modular wallets SDK API
calls.
### Best practices for client keys
Client keys enable access to sensitive application operations, so protecting
them is critical. Follow these best practices:
1. **Use separate keys for each application**: Create separate keys for web and
mobile apps (iOS, Android) to prevent shared vulnerabilities.
2. **Monitor for misuse**: Set up alerts for unusual activity, such as
unexpected spikes in API calls, and use monitoring tools to detect anomalies.
3. **Rotate keys regularly**: Regenerate client keys periodically and update
them in your apps to reduce risk if a key is compromised.
4. **Store keys securely**: Use secure storage options like Local Storage or
Secure Storage for mobile apps, and avoid unnecessary exposure.
5. **Restrict access**: Limit the scope of client keys by associating them with
specific apps or domains to minimize potential misuse.
***
## Kit keys
A kit key is a unique string used to authenticate access for Circle's developer
kits. Kit keys simplify integration by providing a single credential that works
across both testnet and mainnet environments, reducing configuration overhead
when building. Kit keys are free to create and do not require KYC.
**Testnet and mainnet compatibility**
Unlike API keys and client keys, kit keys work on both testnet and mainnet. You
can use the same key during development and in production.
### Keep your kit keys safe
Kit keys enable access to SDK features, so protecting them is essential.
* **Avoid public exposure**: Never share kit keys or include them in client-side
code, public repositories, or other public mediums.
* **Manage securely**: Use your
[Circle Developer account](https://console.circle.com/api-keys) to generate
and manage kit keys. When generating a key, copy it exactly as displayed.
Losing control of your kit key can result in unauthorized access to SDK
capabilities.
# Maker deliver operation failed
Source: https://developers.circle.com/api-reference/stablefx/all/contract-maker-deliver-failed
/openapi/stablefx.yaml webhook contractMakerDeliverFailed
The maker deliver operation on the contract has failed. The maker's deliver transaction failed to confirm onchain.
# Record trade operation failed
Source: https://developers.circle.com/api-reference/stablefx/all/contract-record-trade-failed
/openapi/stablefx.yaml webhook contractRecordTradeFailed
The record trade operation on the contract has failed. The initial onchain recording of the trade was unsuccessful.
# Taker deliver operation failed
Source: https://developers.circle.com/api-reference/stablefx/all/contract-taker-deliver-failed
/openapi/stablefx.yaml webhook contractTakerDeliverFailed
The taker deliver operation on the contract has failed. The taker's deliver transaction failed to confirm onchain.
# Create a quote
Source: https://developers.circle.com/api-reference/stablefx/all/create-quote
/openapi/stablefx.yaml post /v1/exchange/stablefx/quotes
Creates a quote for a trade between two currencies. You should provide an `amount` for the `from` parameter or the `to` parameter, but not for both. Use `type:tradable` for an executable quote or `type:reference` for an indicative quote. Tradable quotes include presign `typedData` for signing.
# Create a webhook subscription
Source: https://developers.circle.com/api-reference/stablefx/all/create-subscription
/openapi/stablefx.yaml post /v2/stablefx/notifications/subscriptions
Create a webhook subscription by configuring an endpoint to receive notifications.
# Create a trade
Source: https://developers.circle.com/api-reference/stablefx/all/create-trade
/openapi/stablefx.yaml post /v1/exchange/stablefx/trades
Accepts a quote and creates a trade.
# Delete a notification subscription
Source: https://developers.circle.com/api-reference/stablefx/all/delete-subscription
/openapi/stablefx.yaml delete /v2/stablefx/notifications/subscriptions/{id}
Delete an existing subscription.
# Fund trades
Source: https://developers.circle.com/api-reference/stablefx/all/fund-trade
/openapi/stablefx.yaml post /v1/exchange/stablefx/fund
Executes funding for trades using Permit2 signatures. This endpoint relays the signed permit data to complete the funding operation for trades.
# Generate funding presign data
Source: https://developers.circle.com/api-reference/stablefx/all/generate-funding-presign-data
/openapi/stablefx.yaml post /v1/exchange/stablefx/signatures/funding/presign
Returns the Permit2 EIP-712 payload that the trader must sign for funding operations. `fundingMode` net option is supported for maker funding requests.
# Generate trade presign data
Source: https://developers.circle.com/api-reference/stablefx/all/generate-trade-signature-data
/openapi/stablefx.yaml get /v1/exchange/stablefx/signatures/presign/{tradeId}
Returns the EIP-712 payload that the trader must sign for a CPS trade.
# Get a notification signature public key
Source: https://developers.circle.com/api-reference/stablefx/all/get-notification-signature
/openapi/stablefx.yaml get /v2/stablefx/notifications/publicKey/{id}
Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle.
In the headers of each webhook, you can find
- `X-Circle-Signature`: a header containing the digital signature generated by Circle.
- `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key.
# Get a notification subscription
Source: https://developers.circle.com/api-reference/stablefx/all/get-subscription
/openapi/stablefx.yaml get /v2/stablefx/notifications/subscriptions/{id}
Returns an existing notification subscription.
# Get all webhook subscriptions
Source: https://developers.circle.com/api-reference/stablefx/all/get-subscriptions
/openapi/stablefx.yaml get /v2/stablefx/notifications/subscriptions
Returns an array of all webhook subscriptions.
# Get a trade
Source: https://developers.circle.com/api-reference/stablefx/all/get-trade-by-id
/openapi/stablefx.yaml get /v1/exchange/stablefx/trades/{tradeId}
Returns a trade specified by the ID path parameter
# Get fee for a trade
Source: https://developers.circle.com/api-reference/stablefx/all/get-trade-fee
/openapi/stablefx.yaml get /v1/exchange/stablefx/fees/{tradeId}
Returns the fee associated with the trade ID provided in the path parameter.
# Get all trades
Source: https://developers.circle.com/api-reference/stablefx/all/list-trades
/openapi/stablefx.yaml get /v1/exchange/stablefx/trades
Returns a paginated list of all trades.
# Register a trade signature
Source: https://developers.circle.com/api-reference/stablefx/all/register-trade-signature
/openapi/stablefx.yaml post /v1/exchange/stablefx/signatures
Registers a signed EIP-712 payload from the trader that confirms trade intent.
# Trade breached
Source: https://developers.circle.com/api-reference/stablefx/all/trade-breached
/openapi/stablefx.yaml webhook tradeBreached
The StableFX trade has breached its maturity date.
# Trade completed
Source: https://developers.circle.com/api-reference/stablefx/all/trade-completed
/openapi/stablefx.yaml webhook tradeCompleted
The StableFX trade has been completed successfully. Both the maker and the taker have funded their side and the trade is fully settled.
# Trade confirmed
Source: https://developers.circle.com/api-reference/stablefx/all/trade-confirmed
/openapi/stablefx.yaml webhook tradeConfirmed
The StableFX trade has been confirmed by the exchange.
# Trade failed
Source: https://developers.circle.com/api-reference/stablefx/all/trade-failed
/openapi/stablefx.yaml webhook tradeFailed
The StableFX trade has failed.
# Trade maker funded
Source: https://developers.circle.com/api-reference/stablefx/all/trade-maker-funded
/openapi/stablefx.yaml webhook tradeMakerFunded
The maker has funded their side of the trade. The maker's fund delivery transaction has been confirmed onchain.
# Trade pending settlement
Source: https://developers.circle.com/api-reference/stablefx/all/trade-pending-settlement
/openapi/stablefx.yaml webhook tradePendingSettlement
The StableFX trade has been confirmed onchain and is awaiting funding from taker and maker.
# Trade refunded
Source: https://developers.circle.com/api-reference/stablefx/all/trade-refunded
/openapi/stablefx.yaml webhook tradeRefunded
The StableFX trade has been refunded.
# Trade taker funded
Source: https://developers.circle.com/api-reference/stablefx/all/trade-taker-funded
/openapi/stablefx.yaml webhook tradeTakerFunded
The taker has funded their side of the trade. The taker's fund delivery transaction has been confirmed onchain.
# Update a notification subscription
Source: https://developers.circle.com/api-reference/stablefx/all/update-subscription
/openapi/stablefx.yaml patch /v2/stablefx/notifications/subscriptions/{id}
Update a notification subscription by configuring an endpoint to receive notifications.
# Retrieve a transfer
Source: https://developers.circle.com/api-reference/wallets/buidl/get-transfer
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/transfers/{id}
Retrieve an existing transfer.
# Retrieve a user operation
Source: https://developers.circle.com/api-reference/wallets/buidl/get-user-op
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/userOps/{id}
Retrieve an existing user operation.
# List transfers
Source: https://developers.circle.com/api-reference/wallets/buidl/list-transfers
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/transfers
Retrieve a list of transfers that fit the specified parameters.
# List user operations
Source: https://developers.circle.com/api-reference/wallets/buidl/list-user-ops
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/userOps
Retrieve a list of all user operations that fit the specified parameters.
# Get wallet balances by blockchain and address
Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-balances-by-blockchain-address
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{blockchain}/{address}/balances
Retrieve wallet balances by blockchain and address.
# Get wallet balances
Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-balances-by-id
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{id}/balances
Retrieve the balances of a wallet by its ID.
# Get wallet NFTs by blockchain and address
Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-nfts-by-blockchain-address
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{blockchain}/{address}/nfts
Retrieve the NFTs of a wallet by applicable blockchain and address.
# Get wallet NFTs
Source: https://developers.circle.com/api-reference/wallets/buidl/list-wallet-nfts-by-id
/openapi/buidl-wallets.yaml get /v1/w3s/buidl/wallets/{id}/nfts
Retrieve the NFTs of a wallet by its ID.
# Create a notification subscription
Source: https://developers.circle.com/api-reference/wallets/common/create-subscription
/openapi/configurations_2.yaml post /v2/notifications/subscriptions
Create a notification subscription by configuring an endpoint to receive notifications. For details, see the [Notification Flows](https://developers.circle.com/wallets/webhook-notification-flows) guide.
# Delete a notification subscription
Source: https://developers.circle.com/api-reference/wallets/common/delete-subscription
/openapi/configurations_2.yaml delete /v2/notifications/subscriptions/{id}
Delete an existing subscription.
# Get a notification signature public key
Source: https://developers.circle.com/api-reference/wallets/common/get-notification-signature
/openapi/configurations_2.yaml get /v2/notifications/publicKey/{id}
Get the public key and algorithm used to digitally sign webhook notifications. Verifying the digital signature ensures the notification came from Circle.
In the headers of each webhook, you can find
1. `X-Circle-Signature`: a header containing the digital signature generated by Circle.
2. `X-Circle-Key-Id`: a header containing the UUID. This is will be used as the `ID` as URL parameter to retrieve the relevant public key.
# Retrieve a notification subscription
Source: https://developers.circle.com/api-reference/wallets/common/get-subscription
/openapi/configurations_2.yaml get /v2/notifications/subscriptions/{id}
Retrieve an existing notification subscription.
# Get all notification subscriptions
Source: https://developers.circle.com/api-reference/wallets/common/get-subscriptions
/openapi/configurations_2.yaml get /v2/notifications/subscriptions
Retrieve an array of existing notification subscriptions.
# Ping
Source: https://developers.circle.com/api-reference/wallets/common/ping
/openapi/configurations_2.yaml get /ping
Checks that the service is running.
# Update a notification subscription
Source: https://developers.circle.com/api-reference/wallets/common/update-subscription
/openapi/configurations_2.yaml patch /v2/notifications/subscriptions/{id}
Update subscription endpoint to receive notifications.
# Screen a blockchain address
Source: https://developers.circle.com/api-reference/wallets/compliance/screen-address
/openapi/compliance.yaml post /v1/w3s/compliance/screening/addresses
Create a screening request for a specific blockchain address and chain.
# Accelerate a transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-accelerate
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/{id}/accelerate
Accelerates a specified transaction from a developer-controlled wallet. Additional gas fees may be incurred.
# Cancel a transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-cancel
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/{id}/cancel
Cancels a specified transaction from a developer-controlled wallet. Gas fees may still be incurred.
This is a best-effort operation, it won't be effective if the original transaction has already been processed by the blockchain.
# Create a contract execution transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-contract-execution
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/contractExecution
Creates a transaction which executes a smart contract. ABI parameters must be passed in the request.
Related transactions may be submitted as a batch transaction in a single call.
You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body.
# Create a transfer transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-transfer
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/transfer
Initiates an on-chain digital asset transfer from a specified developer-controlled wallet.
You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body.
# Create a wallet upgrade transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-developer-transaction-wallet-upgrade
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/transactions/walletUpgrade
Creates a transaction which upgrades a wallet.
# Estimate fee for a contract execution transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-transaction-estimate-fee
/openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/contractExecution/estimateFee
Estimates gas fees that will be incurred for a contract execution transaction, given its ABI parameters and blockchain.
# Estimate fee for a transfer transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-transfer-estimate-fee
/openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/transfer/estimateFee
Estimates gas fees that will be incurred for a transfer transaction; given its amount, blockchain, and token.
# Validate an address
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-validate-address
/openapi/developer-controlled-wallets.yaml post /v1/w3s/transactions/validateAddress
Confirms that a specified address is valid for a given token on a certain blockchain.
# Create wallets
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/wallets
Creates a new developer-controlled wallet or a batch of wallets within a wallet set, given the target blockchain and wallet name.
**Note:** Each `walletSetId` supports a maximum of 10 million wallets.
# Create a new wallet set
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/create-wallet-set
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/walletSets
Creates a new developer-controlled wallet set.
**Note:** A developer account can create up to 1,000 wallet sets, with each set supporting up to 10 million wallets.
To ensure EVM wallets are created with the same address across chains, see [Unified Wallet Addressing on EVM Chains](/w3s/unified-wallet-addressing-evm).
# Derive a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/derive-wallet
/openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/wallets/{id}/blockchains/{blockchain}
Derives an EOA (Externally Owned Account) or SCA (Smart Contract Account) wallet using the address of the specified wallet and blockchain. If the target wallet already exists, its metadata will be updated with the provided metadata. This operation is only supported for EVM-based blockchains.
# Derive wallet by address
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/derive-wallet-by-address
/openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/wallets/derive
Creates a wallet on the target blockchain using the same address as the source wallet identified by source blockchain and wallet address. If the target wallet already exists, its metadata is updated.
# Get fee parameters of a blockchain
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-fee-parameters
/openapi/developer-controlled-wallets.yaml get /v1/w3s/developer/transactions/feeParameters
Get latest fee parameters of a blockchain with an optional account type (default to 'EOA').
# Get the lowest nonce pending transaction for a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-lowest-nonce-transaction
/openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions/lowestNonceTransaction
For a nonce-supported blockchain, get the lowest nonce transaction that's in QUEUED or SENT or STUCK state for the provided wallet.
# Get token details
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-token-id
/openapi/developer-controlled-wallets.yaml get /v1/w3s/tokens/{id}
Fetches details of a specific token given its unique identifier. Every token in your network of wallets has a UUID associated with it, regardless of whether it's already recognized or was added as a monitored token.
# Get a transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-transaction
/openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions/{id}
Retrieves info for a single transaction using it's unique identifier.
# Retrieve a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet
/openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id}
Retrieve an existing wallet
# Get a wallet set
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet-set
/openapi/developer-controlled-wallets.yaml get /v1/w3s/walletSets/{id}
Retrieve an existing wallet set.
# Get all wallet sets
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallet-sets
/openapi/developer-controlled-wallets.yaml get /v1/w3s/walletSets
Retrieve an array of existing wallet sets.
# List wallets
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets
/openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets
Retrieves a list of all wallets that fit the specified parameters.
# List wallets with balances
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/get-wallets-with-balances
/openapi/developer-controlled-wallets.yaml get /v1/w3s/developer/wallets/balances
Retrieves a list of all wallets that match the specified parameters. Wallet balances update automatically after each transfer.
**Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes.
# List transactions
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-transactions
/openapi/developer-controlled-wallets.yaml get /v1/w3s/transactions
Lists all transactions. Includes details such as status, source/destination, and transaction hash.
# Get token balance for a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-balance
/openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id}/balances
Fetches the digital asset balance for a single developer-controlled wallet using its unique identifier.
**Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes.
# Get NFTs for a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-nfts
/openapi/developer-controlled-wallets.yaml get /v1/w3s/wallets/{id}/nfts
Fetches the info for all NFTs stored in a single developer-controlled wallet, using the wallets unique identifier.
# Sign delegate action
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-delegate-action
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/delegateAction
Sign a delegate action from a specific developer-controlled wallet.
NOTE: This endpoint is only available for NEAR and NEAR-TESTNET.
# Sign message
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-message
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/message
Sign a message from a specified developer-controlled wallet. This endpoint supports message signing for Ethereum-based blockchains (using EIP-191), Solana and Aptos (using Ed25519 signatures). Note that Smart Contract Accounts (SCA) are specific to Ethereum and EVM-compatible chains. The difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/."
You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body.
# Sign transaction
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-transaction
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/transaction
Sign a transaction from a specific developer-controlled wallet.
You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body.
NOTE: This endpoint is only available for the following chains: `SOL`, `SOL-DEVNET`, `NEAR`, `NEAR-TESTNET`, `EVM`, `EVM-TESTNET`. Each chain defines its own standard, please refer to [Signing APIs doc](https://learn.circle.com/w3s/signing-apis).
# Sign typed data
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/sign-typed-data
/openapi/developer-controlled-wallets.yaml post /v1/w3s/developer/sign/typedData
| Sign the EIP-712 typed structured data from a specified developer-controlled wallet.
You must provide either a `walletId` or a `walletAddress` and `blockchain` pair in the request body.
This endpoint only supports Ethereum and EVM-compatible blockchains. Please note that not all apps currently support Smart Contract Accounts (SCA); the difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum apps that support SCA: https://eip1271.io/.
# Update a wallet
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/update-wallet
/openapi/developer-controlled-wallets.yaml put /v1/w3s/wallets/{id}
Updates info metadata of a wallet.
# Update a wallet set
Source: https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/update-wallet-set
/openapi/developer-controlled-wallets.yaml put /v1/w3s/developer/walletSets/{id}
Update the name of the wallet set
# Set monitored tokens
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/create-monitored-tokens
/openapi/configurations_1.yaml post /v1/w3s/config/entity/monitoredTokens
Add a new token to the monitored token list.
# Delete monitored tokens
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/delete-monitored-tokens
/openapi/configurations_1.yaml post /v1/w3s/config/entity/monitoredTokens/delete
Delete tokens from the monitored token list.
# Get configuration for entity
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/get-entity-config
/openapi/configurations_1.yaml get /v1/w3s/config/entity
Get the app ID associated to the entity.
# Get public key for entity
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/get-public-key
/openapi/configurations_1.yaml get /v1/w3s/config/entity/publicKey
Get the public key associated with the entity.
# Retrieve existing monitored tokens.
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/list-monitored-tokens
/openapi/configurations_1.yaml get /v1/w3s/config/entity/monitoredTokens
Get monitored tokens
# Request testnet tokens
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/request-testnet-tokens
/openapi/configurations_1.yaml post /v1/faucet/drips
Request testnet tokens for your wallet.
**Note:** Calling the `/v1/faucet/drips` API requires upgrading to mainnet.
# Update monitored tokens
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/update-monitored-tokens
/openapi/configurations_1.yaml put /v1/w3s/config/entity/monitoredTokens
Upsert the monitored token list.
# Update monitored tokens scope
Source: https://developers.circle.com/api-reference/wallets/programmable-wallets/update-monitored-tokens-scope
/openapi/configurations_1.yaml put /v1/w3s/config/entity/monitoredTokens/scope
Select between monitoring all tokens or selected tokens added to the monitored tokens list.
# Get a deviceToken to log in with email OTP
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-device-token-email-login
/openapi/user-controlled-wallets.yaml post /v1/w3s/users/email/token
Get a deviceToken to login with email OTP in SDK
# Get deviceToken to perform social login
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-device-token-social-login
/openapi/user-controlled-wallets.yaml post /v1/w3s/users/social/token
Get deviceToken to perform social login in SDK
# Estimate fee for a contract execution transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-transaction-estimate-fee
/openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/contractExecution/estimateFee
Estimates gas fees that will be incurred for a contract execution transaction, given its ABI parameters and blockchain.
# Estimate fee for a transfer transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-transfer-estimate-fee
/openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/transfer/estimateFee
Estimates gas fees that will be incurred for a transfer transaction; given its amount, blockchain, and token.
# Create a user
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user
/openapi/user-controlled-wallets.yaml post /v1/w3s/users
Create a user.
# Create a challenge for PIN setup
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-pin-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/pin
Creates a challenge for PIN setup without setting up the wallets.
# Create a challenge for PIN restore
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-pin-restore-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/pin/restore
Creates a challenge to restore a user's PIN using security questions.
# Create a Challenge to accelerate a transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-accelerate-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/{id}/accelerate
Generates a challenge to accelerate a specific transaction from a user-controlled wallet. Additional gas fees may apply.
# Create a challenge to cancel a transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-cancel-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/{id}/cancel
Generates a challenge to cancel a specific transaction from a user-controlled wallet. Gas fees may still apply.
# Create a challenge for contract execution
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-contract-execution-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/contractExecution
Generates a challenge for creating a transaction which executes a smart contract. ABI parameters must be passed in the request.
# Create a challenge for a transfer
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-transfer-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/transfer
Generates a challenge for initiating an on-chain digital asset transfer from a specified user-controlled wallet
# Create a challenge for a wallet upgrade
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-transaction-wallet-upgrade-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/transactions/walletUpgrade
Generates a challenge to create a transaction that upgrades a wallet.
# Create wallets
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-wallet
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/wallets
Generates a challenge to create a new user-controlled wallet or a batch of wallets. You must specify the blockchain and wallet name.
# Create a challenge for user initialization with wallet creation
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-user-with-pin-challenge
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/initialize
Creates a challenge for user initialization and creates one or more wallets.
# Validate an address
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/create-validate-address
/openapi/user-controlled-wallets.yaml post /v1/w3s/transactions/validateAddress
Confirms that a specified address is valid for a given token on a certain blockchain.
# Get the lowest nonce pending transaction for a wallet
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-lowest-nonce-transaction
/openapi/user-controlled-wallets.yaml get /v1/w3s/transactions/lowestNonceTransaction
For a nonce-supported blockchain, get the lowest nonce transaction that's in QUEUED or SENT or STUCK state for the provided wallet.
# Get token details
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-token-id
/openapi/user-controlled-wallets.yaml get /v1/w3s/tokens/{id}
Fetches details of a specific token given its unique identifier. Every token in your network of wallets has a UUID associated with it, regardless of whether it's already recognized or was added as a monitored token.
# Get a transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-transaction
/openapi/user-controlled-wallets.yaml get /v1/w3s/transactions/{id}
Retrieves info for a single transaction using it's unique identifier.
# Get a user by ID
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user
/openapi/user-controlled-wallets.yaml get /v1/w3s/users/{id}
Get user by ID.
# Get user
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-by-token
/openapi/user-controlled-wallets.yaml get /v1/w3s/user
Retrieve the user by token.
# Get a challenge
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-challenge
/openapi/user-controlled-wallets.yaml get /v1/w3s/user/challenges/{id}
Retrieve a user challenge.
# Create a user token
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-user-token
/openapi/user-controlled-wallets.yaml post /v1/w3s/users/token
Generate user session and SDK secret key.
# Get a wallet
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/get-wallet
/openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id}
Retrieves info for a single user-controlled wallet using it's unique identifier.
# List transactions
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-transactions
/openapi/user-controlled-wallets.yaml get /v1/w3s/transactions
Lists all transactions. Includes details such as status, source/destination, and transaction hash.
# List challenges
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-user-challenges
/openapi/user-controlled-wallets.yaml get /v1/w3s/user/challenges
List all challenges by status for a user.
# List users
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-users
/openapi/user-controlled-wallets.yaml get /v1/w3s/users
Get all the users under the entity.
# Get token balance for a wallet
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallet-balance
/openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id}/balances
Fetches the digital asset balance for a single user-controlled wallet using its unique identifier.
**Note**: On Aptos, this endpoint only returns balances for tokens stored in primary storage. Tokens held in [AIP-21](https://github.com/aptos-labs/aptos-core/releases/tag/aptos-node-v1.5.0) secondary storage are excluded from balance queries and deposit notifications to prevent incorrect or misleading results from secondary storage-based state changes.
# Get NFTs for a wallet
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallet-nfts
/openapi/user-controlled-wallets.yaml get /v1/w3s/wallets/{id}/nfts
Fetches the info for all NFTs stored in a single user-controlled wallet, using the wallets unique identifier.
# List wallets
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/list-wallets
/openapi/user-controlled-wallets.yaml get /v1/w3s/wallets
Retrieves a list of all user-controlled wallets that fit the specified parameters.
# Get a new userToken with the refreshToken
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/refresh-user-token
/openapi/user-controlled-wallets.yaml post /v1/w3s/users/token/refresh
Get a new userToken with the refreshToken passed over from sdk/performLogin which matches to the current userToken
# Resend an OTP email to the user
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/resend-otp
/openapi/user-controlled-wallets.yaml post /v1/w3s/users/email/resendOTP
When the users don’t receive the OTP email, you can call this API to resend OTP email. The prior OTP email would expire after the new one is sent out.
# Create a challenge to sign message
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-message
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/message
Generates a challenge for signing a message from a specified user-controlled wallet. This endpoint supports Ethereum-based blockchains (using EIP-191), Solana and Aptos (using Ed25519 signatures). Note that Smart Contract Accounts (SCA) are specific to Ethereum and EVM-compatible chains. The difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/.
# Create a challenge to sign transaction
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-transaction
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/transaction
Generate a challenge for signing the transaction from a specific user-controlled wallet.
NOTE: This endpoint supports the following blockchains: SOL, SOL-DEVNET, EVM, EVM-TESTNET. Each chain defines its own standard. For more details, see [Signing APIs](https://developers.circle.com/w3s/signing-apis).
# Create a challenge to sign typed data
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/sign-user-typed-data
/openapi/user-controlled-wallets.yaml post /v1/w3s/user/sign/typedData
Generates a challenge for signing the EIP-712 typed structured data from a specified user-controlled wallet. This endpoint only supports Ethereum and EVM-compatible blockchains. Please note that not all Dapps currently support Smart Contract Accounts (SCA); the difference between Ethereum's EOA and SCA can be found in the [account types guide](https://developers.circle.com/wallets/account-types). You can also check the list of Ethereum Dapps that support SCA: https://eip1271.io/.
# Create a challenge to update PIN
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/update-user-pin-challenge
/openapi/user-controlled-wallets.yaml put /v1/w3s/user/pin
Creates a challenge to update a user's PIN using the current PIN.
# Update a wallet
Source: https://developers.circle.com/api-reference/wallets/user-controlled-wallets/update-wallet
/openapi/user-controlled-wallets.yaml put /v1/w3s/wallets/{id}
Updates info for a single user-controlled wallet using it's unique identifier.
# Get an attestation for a crosschain transfer
Source: https://developers.circle.com/api-reference/xreserve/all/get-attestation
/openapi/xreserve.yaml get /v1/attestations/{depositMessageHash}
Returns the attestation for a specified crosschain transfer using the deposit message hash.
# Get attestations by source transaction hash
Source: https://developers.circle.com/api-reference/xreserve/all/get-attestations-by-tx-hash
/openapi/xreserve.yaml get /v1/attestations
Returns all attestations associated with the specified source chain transaction hash.
A single transaction may produce multiple attestations across different remote domains.
# Get token balances on a remote domain
Source: https://developers.circle.com/api-reference/xreserve/all/get-balances
/openapi/xreserve.yaml get /v1/balances/{remoteDomain}
Returns the expected token balances for a specified remote domain based on the deposit amounts made into xReserve.
# Get domain information
Source: https://developers.circle.com/api-reference/xreserve/all/get-info
/openapi/xreserve.yaml get /v1/info
Returns information on source and remote domains, including its supported tokens and configuration details.
# Get withdrawal status
Source: https://developers.circle.com/api-reference/xreserve/all/get-withdrawal-status
/openapi/xreserve.yaml get /v1/withdrawal/{withdrawalId}
Returns the status and transfer details of a specified withdrawal group.
# List attestations for crosschain transfers
Source: https://developers.circle.com/api-reference/xreserve/all/list-attestations
/openapi/xreserve.yaml get /v1/remote-domains/{remoteDomain}/attestations
Returns an array of attestations for the specified crosschain transfers, filtered by remote domain.
Use the Link header in the response to navigate between pages.
# Prepare a withdrawal request
Source: https://developers.circle.com/api-reference/xreserve/all/prepare-withdrawal
/openapi/xreserve.yaml post /v1/prepare-withdrawal
Turns the user's burn transaction data on the remote chain into fully encoded burn intents to send to the `/withdraw` endpoint. This endpoint performs the following:
- Resolves contract and token addresses across networks.
- Determines the optimal forwarding strategy, such as redepositing to another xReserve remote domain or forwarding to a CCTP domain.
- Calculates transfer amounts and fees.
- Encodes forwarding call data with the calculated transfer amounts.
- Generates `maxBlockHeight` and `maxFee` values with safety buffers.
- Returns data in the structure expected by the withdraw endpoint.
Note: The `/withdraw` endpoint requires signatures and the remote chain `burnTxId` in addition to the burn intent prepared by this endpoint.
# Submit signed burn intents for withdrawal
Source: https://developers.circle.com/api-reference/xreserve/all/submit-withdrawal
/openapi/xreserve.yaml post /v1/withdraw
Submits up to five signed burn intent batches per request call. Each batch may contain one burn intent or a set of up to 10.
# Assets
Source: https://developers.circle.com/assets
Build on trusted digital money: USDC and EURC for open integration, USYC for yield, and xReserve to issue branded, USDC-backed stablecoins.
## Stablecoins
The foundation of programmable money: stable, interoperable assets that move
value across borders and blockchains.
## Tokenized money market
Circle's USYC provides institutions with 24/7 access to a yield-bearing money
market fund settled onchain with real-time subscriptions and redemptions.
## Dive deeper
* Follow the [USDC quickstarts](/stablecoins/quickstarts/transfer-usdc-evm) to
test transfers on any supported L1 or L2.
* Use the [EURC quickstart](/stablecoins/quickstarts/transfer-eurc-evm) to stand
up euro rails in minutes.
* Explore
[USYC subscribe and redeem guides](/tokenized/usyc/subscribe-and-redeem) to
wire funds in and out of tokenized Treasuries.
* Learn about [Circle Mint](/circle-mint) and how institutions can mint USDC and
EURC.
# Build Onchain Experiences
Source: https://developers.circle.com/build-onchain
Compose wallets, contracts, gas sponsorship, and compliance to ship onchain apps faster.
## What you can build
* [**Embedded wallets**](/wallets): build flexible, secure, and scalable wallets
into your application.
* [**Gasless UX**](/wallets/gas-station): give end users a gasless experience
with Gas Station and Paymaster.
* [**Smart contracts**](/contracts): create, deploy, and execute smart contracts
through intuitive APIs.
* [**Compliance operations**](/wallets/compliance-engine): screen transactions
and automate alerts.
## Compose your stack
Mix and match these building blocks based on who controls the keys, how you
cover fees, and the level of automation you need.
Use APIs to create wallets, move funds, and manage policies on behalf of
your users with instant scale.
Ship passkeys, social logins, and custom signing UIs so end users truly own
their keys across devices.
Plug in modules like permissions, recovery, or automation to ship custom
smart-accounts.
Sponsor gas to create a gasless UX for end-users.
Let anyone pay gas directly in USDC through permissionless ERC-4337
paymasters.
Deploy and operate smart contracts via audited templates, APIs, and
monitoring.
Screen transactions, manage rules, and investigate alerts.
## Building something crosschain?
Wallets and contracts become more powerful when they connect across ecosystems.
Use these primitives to move liquidity crosschain and expose a unified balance.
**Use dedicated SDKs for crosschain transfers**
[Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) and
[Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit)
let you move USDC across blockchains without low-level protocol work. Both
support multiple blockchains and wallet providers.
Burn-and-mint native USDC between supported chains with guarantees.
Give users a single USDC balance they can spend anywhere while Circle
handles settlement.
## Dive deeper
* Follow the
[dev-controlled wallet quickstart](/wallets/dev-controlled/create-your-first-wallet)
to stand up API-driven wallets in minutes.
* Use the [user-controlled wallet tutorials](/wallets/user-controlled) to embed
passkey flows and branded signing experiences.
* Head to the [Contracts quickstart](/contracts/scp-deploy-smart-contract) to
deploy an audited template and connect to wallets.
* Explore
[Gas Station quickstarts](/wallets/gas-station/send-a-gasless-transaction) and
[Paymaster guides](/paymaster/pay-gas-fees-usdc) to abstract gas across
networks.
# Cross-Chain Transfer Protocol
Source: https://developers.circle.com/cctp
Cross-Chain Transfer Protocol (CCTP) is a permissionless onchain utility that
facilitates native USDC transfers across blockchains. CCTP burns USDC on the
source blockchain and mints it on the destination blockchain, enabling secure
1:1 transfers without traditional bridge liquidity pools or wrapped tokens.
**Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to
simplify crosschain transfers with CCTP.**
Bridge Kit is a lightweight SDK that uses CCTP as its protocol provider, letting
you transfer USDC between blockchains in just a few lines of code.
## Key features
Transfer native USDC across blockchains without wrapped tokens or liquidity
pools
Choose between [Fast
Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
for speed or [Standard
Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
for cost efficiency
Trigger automated actions on the destination blockchain after USDC arrives
## What you can build
CCTP enables you to build applications that require moving USDC across
blockchains. Here are some common use cases:
Rebalance USDC holdings across blockchains to meet liquidity demands, manage
treasury positions, or take advantage of market opportunities with minimal
latency.
Enable users to swap tokens on one blockchain for tokens on another blockchain
by routing through USDC. Build seamless crosschain trading experiences that feel
like a single transaction.
Accept USDC payments on one blockchain and automatically transfer funds to
another blockchain where your business operations are based or where recipients
prefer to receive funds.
Use CCTP hooks to chain together crosschain actions. Transfer USDC across
blockchains and automatically deposit it into DeFi protocols, purchase NFTs, or
execute smart contract logic.
## Get started
Build a script to transfer USDC between EVM blockchains using CCTP
Transfer USDC from Solana to an EVM blockchain using CCTP
Transfer USDC between Arc and Stellar using CCTP
## Related products
CCTP and Gateway offer different approaches to crosschain transfers. This table
compares the two approaches.
| Attribute | CCTP | Gateway |
| ------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Use case** | Transfer USDC from one blockchain to another | Hold a unified USDC balance accessible on any supported blockchain |
| **Transfer speed** | Fast Transfer: \~8-20 seconds Standard Transfer: 15-19 minutes (Ethereum/L2s) | Instant (\<500 ms) after balance is established |
| **Balance model** | Point-to-point transfers | Unified crosschain balance |
| **Custody** | Non-custodial | Non-custodial with 7-day trustless withdrawal option |
| **Supported blockchains** | [View list](/cctp/concepts/supported-chains-and-domains) | [View list](/gateway/references/supported-blockchains) |
# CCTP-Enabled HyperCore Transfers
Source: https://developers.circle.com/cctp/concepts/cctp-on-hypercore
CCTP lets you transfer USDC from all supported CCTP domains to HyperCore using a
`CoreDepositWallet` contract deployed on HyperEVM. The `CoreDepositWallet`
handles depositing and withdrawing USDC between HyperEVM and HyperCore. This
topic explains how the HyperCore workflow works and covers HyperCore-specific
considerations.
**Note:** HyperCore balances reflect protocol-level credits, not Circle-issued
USDC. Native USDC remains in the `CoreDepositWallet` contract on HyperEVM and
withdrawals are required to [redeem native USDC from
HyperEVM](/cctp/howtos/withdraw-usdc-from-hypercore-to-evm).
## How it works
The HyperCore workflow from source chains that are not HyperEVM is a two-step
process: funds are transferred in the standard CCTP workflow to HyperEVM, then
forwarded to HyperCore by depositing them into the `CoreDepositWallet` contract.
The burn transaction uses one of two contracts depending on the source chain:
* `TokenMessengerV2` contract
* `CctpExtension` contract
The burn transaction includes a hook that calls the `CctpForwarder` contract on
HyperEVM to forward the USDC to the recipient address on HyperCore.
Here's how the HyperCore deposit workflow works:
1. Check the CCTP API for fees. Fast transfers from Arbitrum with a HyperCore
destination have no fast transfer fee. Using Circle's Forwarder Service to
forward to HyperCore is optional and has a dynamic destination chain gas fee.
2. Calculate the USDC amounts minus fees.
3. Approve the contract to spend the amount of USDC you want to burn. If you are
interacting with the `TokenMessengerV2` contract, you can do this with a call
to the `approve` function on the USDC contract. If you are interacting with
the `CctpExtension` contract on Arbitrum, you sign a
`ReceiveWithAuthorization` message.
4. Sign and broadcast a burn transaction. The type depends on the source domain:
* **CctpExtension contract on Arbitrum**: Sign and broadcast a
`batchDepositForBurnWithAuth` transaction. Set HyperEVM as the destination.
Include hook data to call the `CctpForwarder` contract on HyperEVM.
* **TokenMessengerV2 contract**: Sign and broadcast a `depositForBurn`
transaction. Set HyperEVM as the destination. Include hook data to call the
`CctpForwarder` contract on HyperEVM.
HyperCore deposits and withdrawals are supported to/from any CCTP-supported
blockchain. Forwarding for these transfers is optional and is supported for all
[Forwarder-supported blockchains](/cctp/concepts/supported-chains-and-domains#supported-blockchains)
except for withdrawals to Solana.
## Important considerations
Keep these things in mind when using CCTP with HyperCore.
### Testnet recipient address limitations
When you test USDC transfers to HyperCore on testnet, the recipient address has
limits:
* The recipient address must already exist on HyperCore mainnet.
* Addresses that already exist on mainnet can only receive up to \$1000 testnet
USDC.
* Transfers to addresses without mainnet state fail silently.
To check if an address exists on mainnet, use Hyperliquid's info API:
```shell theme={null}
curl -X POST https://api.hyperliquid.xyz/info \
-H "Content-Type: application/json" \
-d '
{
"type": "userRole",
"user": "${USER_ADDRESS}"
}
'
```
### Account activation fee on HyperCore
New HyperCore accounts are subject to a one-time 1 USDC activation fee, managed
entirely by the Hyperliquid protocol. Circle's `CoreDepositWallet` contract does
not collect or enforce this fee.
When a new user first deposits USDC to HyperCore, the full deposit amount is
credited to their tradable balance. The 1 USDC activation fee is earmarked at
the account level and charged on the user's first outbound action, such as a
withdrawal or `SendAsset` transfer. Until that first outbound action, the
account is considered unactivated and cannot perform
[CoreWriter actions](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/activation-gas-fee).
This means:
* There is no minimum deposit amount. Deposits of any size, including less than
1 USDC, will succeed.
* The user's first outbound action (withdrawal, transfer, etc.) requires a
balance of at least 1 USDC. If the balance is below 1 USDC at that time, the
action will fail.
* After the activation fee is paid, subsequent outbound actions are not subject
to it.
The activation fee is separate from CCTP forwarding fees. When calculating
total costs for a new user's first withdrawal, account for both the 1 USDC
activation fee (charged by Hyperliquid) and any applicable CCTP forwarding
fee.
### Best practices for new account deposits
When your integration deposits USDC to a new HyperCore account:
* Ensure the deposit is large enough that the recipient will have at least 1
USDC available for their first outbound action.
* Inform end users that their first withdrawal or transfer from HyperCore
includes a one-time 1 USDC activation fee deducted by the Hyperliquid
protocol.
* If your integration creates accounts programmatically (for example, contract
addresses), you can pre-activate the account by sending an activation
transaction to the EVM contract address on HyperCore, as described in
[Hyperliquid's documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/activation-gas-fee).
# Fast Transfer Allowance
Source: https://developers.circle.com/cctp/concepts/fast-transfer-allowance
Understanding Circle's Fast Transfer allowance mechanism
The Fast Transfer allowance is Circle's mechanism for providing
faster-than-finality USDC transfers. It limits the total value of USDC that can
be minted through Fast Transfer before related burns reach hard finality.
## How it works
Circle maintains a Fast Transfer allowance pool that backs all in-process CCTP
Fast Transfers. The following steps describe how the allowance works:
1. **Initial state**: Circle maintains a Fast Transfer allowance pool (for
example, 10 million USDC).
2. **Fast Transfer initiated**: When you burn USDC on the source blockchain with
Fast Transfer:
* The burn amount temporarily debits the allowance
* Circle's Attestation Service issues an attestation after
[soft finality](/cctp/concepts/finality-and-block-confirmations)
* You can immediately mint USDC on the destination blockchain
3. **Allowance depleted**: If the allowance reaches zero, Fast Transfers are
temporarily unavailable until the allowance replenishes.
4. **Allowance replenished**: Once burns reach hard finality on source
blockchains, the corresponding amounts are credited back to the allowance.
**Note:** The Fast Transfer allowance is global across all supported
blockchains. It's not specific to a particular source or destination blockchain,
but rather tracks the total value of in-process Fast Transfers.
## Check the current allowance
To check the remaining Fast Transfer allowance, call the
[`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance)
endpoint. For a detailed guide on checking the allowance, see
[Get the Fast Transfer allowance](/cctp/howtos/get-fast-transfer-allowance).
## When allowance is insufficient
If the Fast Transfer allowance is insufficient for your transfer, you have two
options:
### Option 1: Wait for replenishment
The allowance automatically replenishes as pending Fast Transfers reach hard
finality.
Monitor the allowance until sufficient capacity is available:
```ts TypeScript theme={null}
async function waitForAllowance(requiredAmount: bigint, timeoutMs = 1200000) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/fastBurn/USDC/allowance",
);
const { allowance } = await response.json();
// Convert USDC string to 6-decimal subunits without float precision loss
const [whole, frac = ""] = String(allowance).split(".");
const allowanceSubunits =
BigInt(whole) * 1_000_000n + BigInt((frac + "000000").slice(0, 6));
if (allowanceSubunits >= requiredAmount) {
console.log("Sufficient allowance available");
return true;
}
console.log(`Current allowance: ${allowance} USDC, waiting...`);
await new Promise((resolve) => setTimeout(resolve, 30000)); // Check every 30 seconds
}
throw new Error("Timeout waiting for allowance replenishment");
}
```
### Option 2: Use Standard Transfer
Change `minFinalityThreshold` to 2000 or higher to use
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times),
which doesn't consume the Fast Transfer allowance.
## Allowance lifecycle
Understanding what happens during each phase of the allowance lifecycle helps
you build more robust applications:
The user calls `depositForBurn` with `minFinalityThreshold` ≤ 1000. The
transaction confirms on the source blockchain, and the allowance is debited by
the burn amount.
Circle's Attestation Service issues an attestation, and the attestation becomes
available through the API. The user can now mint USDC on the destination
blockchain.
The burn transaction reaches hard finality on the source blockchain. The
allowance is credited back by the burn amount, and capacity is restored for new
Fast Transfers.
# CCTP Fees
Source: https://developers.circle.com/cctp/concepts/fees
Understanding CCTP transfer fees for Fast and Standard transfers
CCTP charges fees on Fast Transfers only. Standard Transfers are free.
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
enables USDC transfers at faster-than-finality speeds by leveraging Circle's
[Fast Transfer allowance](/cctp/concepts/fast-transfer-allowance). These
transfers incur a fee that varies by route.
* **Fee range**: 0-14 basis points depending on source blockchain, for example
\$0–\$1.40 per \$1,000 transferred
* **When and how the fee is collected**: The fee is deducted from the
transferred amount when USDC is minted on the destination blockchain
## Get the current fee
To retrieve the current Fast Transfer fee for your route, call the
[`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) endpoint.
For more details, see
[Get the fee for your transfer](/cctp/howtos/get-transfer-fee).
## Maximum fee parameter
When calling
[`depositForBurn`](/cctp/references/contract-interfaces#depositforburn), you
specify a `maxFee` parameter that sets the maximum fee you're willing to pay:
```ts TypeScript theme={null}
await tokenMessenger.depositForBurn(
amount,
destinationDomain,
mintRecipient,
burnToken,
destinationCaller,
500n, // maxFee: 500 subunits (0.0005 USDC)
1000, // minFinalityThreshold: Fast Transfer
);
```
If the actual fee exceeds your specified `maxFee`, the transaction will revert
on the source blockchain, and no USDC will be burned.
To avoid transaction failures:
1. Retrieve the current fee before initiating a transfer
2. Add a small buffer (for example, 10-20%) to account for potential fee
fluctuations
3. Set `maxFee` to this buffered amount
Example:
```ts TypeScript theme={null}
async function calculateMaxFee(
sourceDomain: number,
destDomain: number,
transferAmountUSDC: string, // USDC amount like "1" or "10.5"
) {
// Convert USDC to subunits (6 decimals)
const [whole, decimal = ""] = transferAmountUSDC.split(".");
const decimal6 = (decimal + "000000").slice(0, 6);
const transferAmount = BigInt(whole + decimal6);
// Get current fee
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${sourceDomain}/${destDomain}`,
);
const fees = await response.json();
// Extract minimumFee for Fast Transfer (finalityThreshold 1000)
const minimumFee = fees[0].minimumFee; // Fee in basis points
// Calculate fee as percentage of transfer amount
const protocolFee =
(transferAmount * BigInt(Math.round(minimumFee * 100))) / 1_000_000n;
// Add 20% buffer to protocol fee (protocolFee × 1.2) - result in subunits
const maxFee = (protocolFee * 120n) / 100n;
return maxFee; // denominated in USDC subunits (6 decimals)
}
// Use in your burn call
const maxFee = await calculateMaxFee(0, 1, "10.5");
```
## Fee tables
The following tables show the current fee rates by source blockchain for Fast
and Standard Transfers. Fees are subject to change at any time.
**Do not hardcode fee values.** Fees can change at any time. Always retrieve the
current fee by calling the [fee API](/api-reference/cctp/all/get-burn-usdc-fees)
at least once per week. Hardcoding fees can cause:
* **Insufficient fees**: If fees increase, your Fast Transfers may be degraded
to Standard Transfers when the provided `maxFee` is below the required
threshold.
* **Overstated fees**: If fees decrease, users may see higher fees than
necessary in your UI, even though the excess is refunded during minting.
| Source blockchain | Fee |
| ----------------- | ---------------- |
| Arbitrum | 1.3 bps (0.013%) |
| Base | 1.3 bps (0.013%) |
| Codex | 1.5 bps (0.015%) |
| EDGE | 1.5 bps (0.015%) |
| Ethereum | 1 bps (0.01%) |
| Ink | 2 bps (0.02%) |
| Linea | 11 bps (0.11%) |
| Morph | 4 bps (0.04%) |
| OP Mainnet | 1.3 bps (0.013%) |
| Plume | 2 bps (0.02%) |
| Solana | 1 bps (0.01%) |
| Starknet | 14 bps (0.14%) |
| Unichain | 1.5 bps (0.015%) |
| World Chain | 1.3 bps (0.013%) |
**Blockchains without Fast Transfer fees**
Some blockchains don't appear in the Fast Transfer fee table because their
standard attestation times are already fast enough. Consequently, Fast Transfer
is not applicable when these blockchains are used as the source blockchain for
burns. For affected blockchains, see
[CCTP supported blockchains](/cctp/concepts/supported-chains-and-domains).
| Source blockchain | Fee |
| ----------------- | ---------- |
| Arbitrum | 0 bps (0%) |
| Arc Testnet | 0 bps (0%) |
| Avalanche | 0 bps (0%) |
| Base | 0 bps (0%) |
| Codex | 0 bps (0%) |
| EDGE | 0 bps (0%) |
| Ethereum | 0 bps (0%) |
| HyperEVM | 0 bps (0%) |
| Injective | 0 bps (0%) |
| Ink | 0 bps (0%) |
| Linea | 0 bps (0%) |
| Monad | 0 bps (0%) |
| Morph | 0 bps (0%) |
| OP Mainnet | 0 bps (0%) |
| Pharos | 0 bps (0%) |
| Plume | 0 bps (0%) |
| Polygon PoS | 0 bps (0%) |
| Sei | 0 bps (0%) |
| Solana | 0 bps (0%) |
| Sonic | 0 bps (0%) |
| Starknet | 0 bps (0%) |
| Unichain | 0 bps (0%) |
| World Chain | 0 bps (0%) |
| XDC | 0 bps (0%) |
## Standard Transfer fee switch
Some blockchains support a Standard Transfer fee switch, which enables enforcing
a minimum fee during a CCTP Standard Transfer.
* Some deployments of the `TokenMessengerV2` contract include a fee switch that
enforces a minimum onchain fee. This fee is collected during USDC minting in a
Standard Transfer. See tables below for supported blockchains.
* `TokenMessengerV2` contracts with fee switch support include the
`getMinFeeAmount` function, which calculates and returns the minimum fee
required for a given burn amount, in units of the `burnToken`.
**Important:** Calling `getMinFeeAmount` on a blockchain that uses an older
`TokenMessengerV2` contract (without fee switch support) results in an error.
Refer to the tables below to determine which contract version is deployed on
each EVM blockchain.
### `TokenMessenger` contracts without fee switch support
| Source blockchain | Contract source code |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| Arbitrum | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Avalanche | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Base | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Codex | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Ethereum | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Linea | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| OP Mainnet | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Polygon PoS | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Sonic | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| Unichain | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
| World Chain | [`7d70310`](https://github.com/circlefin/evm-cctp-contracts/pull/57/commits/7d703109a2cfcb3f76375fef5f1a97f03c447b94) |
### `TokenMessenger` contracts with fee switch support
| Source blockchain | Contract source code |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| Sei | [`2f9a2ba`](https://github.com/circlefin/evm-cctp-contracts/commit/2f9a2ba993b96a442c75bf21b3cb6d6292d81439) |
## Fee optimization strategies
To minimize fees while maximizing transfer speed:
* **Choose the right method**: Use Fast Transfer when speed is critical and
Standard Transfer when cost optimization is the priority.
* **Monitor allowance**: For high-volume applications, monitor the
[Fast Transfer allowance](/cctp/concepts/fast-transfer-allowance) and switch
to Standard Transfer when it's low.
* **Batch transfers**: If you're making multiple transfers, consider batching
them during periods when Fast Transfer allowance is high.
* **Set appropriate `maxFee`**: Always retrieve the current fee before
initiating a transfer and set `maxFee` with a buffer to account for minor
fluctuations.
# Finality and Block Confirmations
Source: https://developers.circle.com/cctp/concepts/finality-and-block-confirmations
Block confirmation requirements and attestation timing for CCTP
Before signing an attestation, Circle waits for blockchain transactions to
achieve the appropriate level of transaction finality. The required finality
level depends on whether you use Fast Transfer or Standard Transfer.
* Fast Transfer: Attestations are issued after the transaction is confirmed and
included in a block, typically in seconds. Because of the faster finality
time, Fast Transfers are subject to a global allowance to mitigate
reorganization risks.
* Standard Transfer: Attestations are issued after hard finality, when the
transaction is unlikely to be reversed by a chain reorganization, typically in
minutes.
## Fast Transfer attestation times
The table below shows the average time for attestations to become available when
using Fast Transfer (`minFinalityThreshold` ≤ 1000):
| Source blockchain | Block confirmations | Average time |
| ----------------- | ------------------- | ------------ |
| **Ethereum** | 2 | \~20 seconds |
| **Arbitrum** | 1 | \~8 seconds |
| **Base** | 1 | \~8 seconds |
| **Codex** | 1 | \~8 seconds |
| **EDGE** | 1 | \~8 seconds |
| **Ink** | 1 | \~8 seconds |
| **Linea** | 1 | \~8 seconds |
| **Morph** | 1 | \~8 seconds |
| **OP Mainnet** | 1 | \~8 seconds |
| **Plume** | 1 | \~8 seconds |
| **Solana** | 2-3 | \~8 seconds |
| **Starknet** | 4 | \~20 seconds |
| **Unichain** | 1 | \~8 seconds |
| **World Chain** | 1 | \~8 seconds |
**Blockchains without Fast Transfer:**
Some blockchains don't support Fast Transfer as a source blockchain because
their standard attestation times are already fast. For those
[CCTP supported blockchains](/cctp/concepts/supported-chains-and-domains) where
Fast Transfer is disabled as a source, use Standard Transfer instead.
## Standard Transfer attestation times
The table below shows the average time for attestations to become available when
using Standard Transfer (`minFinalityThreshold` ≥ 2000):
| Source blockchain | Block confirmations | Average time |
| ------------------- | ------------------- | --------------- |
| **Ethereum** | \~65 | \~15-19 minutes |
| **Arbitrum** | \~65 ETH blocks | \~15-19 minutes |
| **Arc Testnet** | 1 | \~0.5 seconds |
| **Avalanche** | 1 | \~8 seconds |
| **Base** | \~65 ETH blocks | \~15-19 minutes |
| **BNB Smart Chain** | 3 | \~2 seconds |
| **Codex** | \~65 ETH blocks | \~15-19 minutes |
| **EDGE** | \~65 ETH blocks | \~16-21 minutes |
| **HyperEVM** | 1 | \~5 seconds |
| **Injective** | 1 | \~0.65 seconds |
| **Ink** | \~65 ETH blocks | \~30 minutes |
| **Linea** | 1 | \~6-32 hours |
| **Monad** | 1 | \~5 seconds |
| **Morph** | \~65 ETH blocks | \~20-30 minutes |
| **OP Mainnet** | \~65 ETH blocks | \~15-19 minutes |
| **Pharos** | 1 | \~7 seconds |
| **Plume** | \~65 ETH blocks | \~15-19 minutes |
| **Polygon PoS** | 2-3 | \~8 seconds |
| **Sei** | 1 | \~5 seconds |
| **Solana** | 32 | \~25 seconds |
| **Sonic** | 1 | \~8 seconds |
| **Starknet** | \~65 ETH Blocks | \~4 to 8 hours |
| **Stellar** | 1 | \~5 seconds |
| **Unichain** | \~65 ETH blocks | \~15-19 minutes |
| **World Chain** | \~65 ETH blocks | \~15-19 minutes |
| **XDC** | 3 | \~10 seconds |
## Layer 2 finality
Layer 2 (L2) blockchains built on Ethereum publish transaction data in batches
to Ethereum Layer 1. The finality characteristics of L2 chains depend on when
batches are posted and when those batches achieve finality on Ethereum L1.
OP Stack-based chains (including Base, OP Mainnet, and World Chain) post
state updates using [EIP-4844](https://www.eip4844.com/) blob transactions
approximately every 15 minutes. Circle waits for the Ethereum L1 block
containing the batch to finalize, which typically takes \~65 blocks (15-19
minutes) after the batch is posted.
Linea has a longer finality period compared to other L2 chains. Standard
Transfer on Linea typically requires 6-32 hours before attestations become
available.
The typical time to reach hard finality on Starknet is 4–8 hours, as finality
depends on when the zk-rollup proof is posted to Ethereum and when its
corresponding L1 block finalizes.
## Solana finality
Solana uses a different finality model:
Circle waits for the block to be confirmed (votes from validators
representing over two-thirds of total stake). This typically takes 2-3
blocks (\~8 seconds).
Circle waits for block finality, which takes 32 blocks (\~25 seconds).
# Circle Forwarding Service for CCTP
Source: https://developers.circle.com/cctp/concepts/forwarding-service
Forward destination chain mints to simplify crosschain transfers
The Circle Forwarding Service is a service for CCTP that simplifies integration
by removing the need for you to run multichain infrastructure. This can improve
user experience for crosschain transfers by ensuring reliability and eliminating
the need to handle destination chain gas fees.
## How it works
A CCTP transfer without the Forwarding Service is a three-step process:
1. Create a transaction to burn USDC on the source chain and wait for Circle to
sign an attestation.
2. Request an attestation from the Circle API.
3. Create a transaction to mint USDC on the destination chain.
This process requires you to have a wallet that can sign transactions on the
source and destination chains, and native tokens for paying the transaction gas
fee on both chains.
You use the Forwarding Service by including a forward request in the hook data
of the burn transaction on the source chain. Circle validates the hook data,
signs the attestation, and broadcasts the mint transaction on the destination
chain for you, removing the need for you to handle the transaction on the
destination chain.
For a full example of how to use the Forwarding Service, see
[Transfer USDC with the Forwarding Service](/cctp/howtos/transfer-usdc-with-forwarding-service).
### Hook format
The hook data for Forwarding Service begins with the reserved magic bytes
`cctp-forward` followed by versioning and payload fields. You can append your
own custom hook data after Circle's reserved space. Forwarding Service doesn't
support forwarding to wrapper contracts (for example, when `destinationCaller`
is set).
| Bytes | Type | Data |
| ----- | --------- | ------------------------------------------------- |
| 0-23 | `bytes24` | `cctp-forward` |
| 24-27 | `uint32` | Version, set to `0` |
| 28-31 | `uint32` | Length of additional Circle hook data, set to `0` |
| 32-51 | `any` | Developer-defined hook data |
If no additional integrator hook data is required, a static hex string can be
used for the forwarding hook data:
```javascript theme={null}
// Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0)
const forwardHookData =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
```
### Solana `mintRecipient`
When the destination blockchain is Solana, the `mintRecipient` parameter in
`depositForBurnWithHook` must be the recipient's USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account)
address, not the recipient's wallet address. Unlike EVM destinations where
`mintRecipient` is the wallet address, Solana requires the address of the SPL
token account that will hold the minted USDC.
You can derive the ATA address from the recipient's wallet address and the USDC
mint address using the
[`getAssociatedTokenAddressSync`](https://solana-labs.github.io/solana-program-library/token/js/functions/getAssociatedTokenAddressSync.html)
function from the `@solana/spl-token` library:
```typescript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
const recipientWallet = new PublicKey("RecipientWalletAddress");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet
const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet);
const mintRecipient = `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}`;
```
### Solana hook data for ATA creation
If the recipient does not have an existing USDC ATA, you can request the
Forwarding Service to create it by encoding additional fields in the hook data.
The extended hook data format is:
| Bytes | Type | Data |
| ----- | --------- | -------------------------------------------------- |
| 0-23 | `bytes24` | `cctp-forward` |
| 24-27 | `uint32` | Version, set to `0` |
| 28-31 | `uint32` | Length of additional Circle hook data, set to `33` |
| 32 | `uint8` | `1` (request ATA creation) |
| 33-64 | `bytes32` | Recipient wallet address (ATA owner) |
| 65+ | `any` | Developer-defined hook data |
When using this format, the `mintRecipient` must be the ATA derived from the
wallet address in bytes 33-64 and the USDC mint. The Forwarding Service
validates that these values are consistent before creating the account.
The following example shows how to construct the extended hook data for Solana
with ATA creation:
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
// Magic bytes "cctp-forward" padded to 24 bytes
const magicBytes = Buffer.alloc(24);
magicBytes.write("cctp-forward", "utf-8");
// Version (uint32, big-endian) = 0
const version = Buffer.alloc(4);
// Length of additional Circle hook data (uint32, big-endian) = 33
const length = Buffer.alloc(4);
length.writeUInt32BE(33);
// ATA creation flag = 1
const ataFlag = Buffer.from([1]);
// Recipient wallet address (32 bytes)
const recipientWallet = new PublicKey("RecipientWalletAddress");
const walletBytes = Buffer.from(recipientWallet.toBytes());
const forwardHookData =
"0x" +
Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString(
"hex",
);
```
If the recipient already has a USDC ATA and no ATA creation is needed, use the
same static hook data as EVM:
```typescript theme={null}
// Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0)
const forwardHookData =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
```
## Fees and execution
The Forwarding Service charges a fee for each transfer, in addition to the CCTP
protocol fee. The Forwarding Service fee charged is to cover gas costs on the
destination chain and a small service fee. The Forwarding Service prioritizes
fast execution and quotes gas dynamically. If gas used is less than gas needed
for execution, the remainder is spent as an additional priority fee where they
are supported. On all chains, a higher fee provides a safety buffer for
successful transaction delivery on the destination chain. Circle does not refund
for excess gas and does not keep the excess gas, except in cases where excess
priority fees are rejected.
The
[`depositForBurnWithHook`](/cctp/references/contract-interfaces#depositforburnwithhook)
transaction includes a `maxFee` parameter. When using the Forwarding Service,
this parameter should be set to a value that is large enough to cover the CCTP
protocol fee and the Forwarding Service fee. Because the gas budget for the
destination chain comes from a USDC fee on the source chain, choosing a lower
`maxFee` results in a lower priority fee on the destination chain. A higher
`maxFee` results in a higher priority fee on the destination chain and can
result in faster confirmation.
The Forwarding Service charges a service fee for each transfer:
| Destination chain | Service fee (USDC) |
| ----------------- | ------------------ |
| All chains | \$0.20 |
If the `maxFee` parameter is insufficient to cover the both Fast Transfer
protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding
execution over Fast Transfer. This means that the transfer will execute as a
Standard Transfer with the Forwarding Service.
### Solana fees
When forwarding to Solana, the Forwarding Service fee includes both a gas
component and a rent component. Rent covers the cost of creating onchain
accounts required by each transfer. The fee estimate API returns `forwardFee`
values that already include rent, so no additional calculation is needed on your
part.
If the recipient does not already have a USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account)
on Solana, the transfer will fail. To have the Forwarding Service create the
ATA, you must:
1. Pass `includeRecipientSetup=true` when calling the fee estimate API so the
returned `forwardFee` covers the ATA creation cost.
2. Encode the ATA creation fields in the
[hook data](#solana-hook-data-for-ata-creation) of the burn transaction.
```http theme={null}
GET /v2/burn/USDC/fees/{sourceDomain}/{destDomain}?forward=true&includeRecipientSetup=true
```
For full details on this endpoint, see the
[`GET /v2/burn/USDC/fees` API reference](/api-reference/cctp/all/get-burn-usdc-fees).
`includeRecipientSetup` only applies when the destination blockchain is Solana.
It has no effect for EVM destination blockchains.
## Supported blockchains
For a full list of supported blockchains, see
[CCTP Supported Blockchains](/cctp/concepts/supported-chains-and-domains).
# Supported Blockchains and Domains
Source: https://developers.circle.com/cctp/concepts/supported-chains-and-domains
Blockchains and domain identifiers supported by CCTP
CCTP is available on multiple blockchains where USDC is natively issued. Each
blockchain is assigned a unique domain identifier used in
[CCTP contracts](/cctp/references/contract-addresses) and API calls.
## Supported blockchains
CCTP provides
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times),
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times),
Hooks, and [Forwarding Service](/cctp/concepts/forwarding-service) capabilities
on the following blockchains. All chains listed below are supported as
destination chains.
| Blockchain | Source (Standard transfer) | Source (Fast transfer) | Forwarding Service |
| --------------------------- | -------------------------- | ---------------------- | ------------------ |
| Arc Testnet | ✅ | ❌ | ✅ |
| Arbitrum | ✅ | ✅ | ✅ |
| Avalanche | ✅ | ❌ | ✅ |
| Base | ✅ | ✅ | ✅ |
| BNB Smart Chain (USYC only) | ✅ | ✅ | ❌ |
| Codex | ✅ | ✅ | ✅ |
| EDGE | ✅ | ✅ | ✅ |
| Ethereum | ✅ | ✅ | ✅ |
| HyperEVM | ✅ | ❌ | ✅ |
| Injective | ✅ | ❌ | ❌ |
| Ink | ✅ | ✅ | ✅ |
| Linea | ✅ | ✅ | ✅ |
| Monad | ✅ | ❌ | ✅ |
| Morph | ✅ | ✅ | ❌ |
| OP Mainnet | ✅ | ✅ | ✅ |
| Pharos | ✅ | ❌ | ❌ |
| Plume | ✅ | ✅ | ✅ |
| Polygon PoS | ✅ | ❌ | ✅ |
| Sei | ✅ | ❌ | ✅ |
| Solana | ✅ | ✅ | ✅ |
| Sonic | ✅ | ❌ | ✅ |
| Starknet | ✅ | ✅ | ❌ |
| Stellar | ✅ | ✅ | ❌ |
| Unichain | ✅ | ✅ | ✅ |
| World Chain | ✅ | ✅ | ✅ |
| XDC | ✅ | ❌ | ✅ |
On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. For inbound transfers, use
[`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
so funds reach the correct recipient. See
[CCTP on Stellar](/cctp/references/stellar).
**Forwarding Service support:**
The column labeled "Forwarding Service" indicates whether the blockchain is
available as a destination chain for the
[Circle Forwarding Service](/cctp/concepts/forwarding-service).
**Testnet support:**
If a mainnet is listed, its official testnet is also supported. For example,
Ethereum includes both Ethereum Mainnet and Ethereum Sepolia.
**Fast Transfer availability:**
[Fast Transfer](/cctp/concepts/fast-transfer-allowance) is available for source
chains only when it provides a meaningful speed improvement over standard burn
attestation times. For blockchains where standard attestation is already fast,
Fast Transfer does not provide additional value.
## Domain identifiers
A domain is a Circle-issued identifier for a blockchain where CCTP contracts are
deployed. Domain identifiers don't map to existing public chain IDs.
Use domain identifiers when calling CCTP contracts and API endpoints:
| Domain | Blockchain |
| :----- | :-------------- |
| 0 | Ethereum |
| 1 | Avalanche |
| 2 | OP Mainnet |
| 3 | Arbitrum |
| 5 | Solana |
| 6 | Base |
| 7 | Polygon PoS |
| 10 | Unichain |
| 11 | Linea |
| 12 | Codex |
| 13 | Sonic |
| 14 | World Chain |
| 15 | Monad |
| 16 | Sei |
| 17 | BNB Smart Chain |
| 18 | XDC |
| 19 | HyperEVM |
| 21 | Ink |
| 22 | Plume |
| 25 | Starknet |
| 26 | Arc Testnet |
| 27 | Stellar |
| 28 | EDGE |
| 29 | Injective |
| 30 | Morph |
| 31 | Pharos |
## Supported tokens
Not all domains support the same tokens:
* [USDC](/stablecoins/what-is-usdc): Supported on all CCTP domains except BNB
Smart Chain
* [USYC](/tokenized/usyc/overview): Supported only on Ethereum and BNB Smart
Chain
## CCTP V1 (Legacy) only
The following blockchains are supported only by CCTP V1 (Legacy). If you are
building on these chains, refer to the [V1 documentation](/cctp/v1) for
integration guides and contract references.
| Blockchain | Domain | Documentation |
| ---------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| Aptos | 9 | [Aptos packages](/cctp/v1/aptos-packages), [Quickstart](/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base) |
| Noble | 4 | [Noble Cosmos module](/cctp/v1/noble-cosmos-module) |
| Sui | 8 | [Sui packages](/cctp/v1/sui-packages), [Quickstart](/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum) |
# Get the Fast Transfer Allowance
Source: https://developers.circle.com/cctp/howtos/get-fast-transfer-allowance
Check the remaining Fast Transfer allowance for USDC transfers
This how-to shows you how to retrieve the remaining Fast Transfer allowance
using the [CCTP API](/api-reference/cctp/all/get-fast-burn-usdc-allowance). The
Fast Transfer allowance is Circle's mechanism for backing faster-than-finality
USDC transfers before burns reach hard finality on source chains.
## Prerequisites
Before you begin, ensure you have:
* Installed cURL on your development machine
## Get the Fast Transfer allowance
Call the
[`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance)
endpoint to retrieve the current remaining Fast Transfer allowance.
**Example request**
```shell Shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/fastBurn/USDC/allowance' \
--header 'Accept: application/json'
```
**Response**
```json theme={null}
{ "allowance": 99999999225.24174, "lastUpdated": "2025-12-02T13:17:02.453Z" }
```
The response includes:
* `allowance`: The remaining Fast Transfer allowance in USDC units
* `lastUpdated`: The UTC timestamp when the allowance was last updated
**Fast Transfer allowance details:**
* The allowance represents the total value of USDC that can be minted through
Fast Transfer before related burns on source chains reach hard finality.
* When you initiate a Fast Transfer, the burn amount temporarily debits the
allowance.
* Once the burn reaches finality on the source chain, the corresponding amount
is credited back to the allowance.
* If the allowance is insufficient for your transfer, you should either wait for
the allowance to replenish or use Standard Transfer instead.
# Get the Fee for Your Transfer
Source: https://developers.circle.com/cctp/howtos/get-transfer-fee
Retrieve CCTP transfer fees using the API
This guide shows you how to retrieve the fee for a USDC transfer using the CCTP
API. Fees vary based on the source and destination blockchains, and whether you
use Fast Transfer or Standard Transfer.
## Prerequisites
Before you begin, ensure you have:
* Installed cURL on your development machine
## Get the transfer fee
Call the [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees)
endpoint to retrieve the fees for transferring USDC between two blockchains.
**Request parameters**
* `sourceDomainId`: The
[domain ID](/cctp/concepts/supported-chains-and-domains#domain-identifiers) of
the source blockchain
* `destDomainId`: The domain ID of the destination blockchain
**Example request**
```shell Shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/0/26' \
--header 'Accept: application/json'
```
This example retrieves the fees for transferring USDC from Ethereum Sepolia
(domain 0) to Arc Testnet (domain 26).
**Response**
```json theme={null}
[
{ "finalityThreshold": 1000, "minimumFee": 1 },
{ "finalityThreshold": 2000, "minimumFee": 0 }
]
```
**Fee details:**
* Fees are specified in basis points (bps), for example, 1 = 0.01%.
* Fast Transfer fees vary by route.
* You specify the maximum fee you're willing to pay when calling
`depositForBurn`. The actual fee charged will not exceed this amount.
# Resolve Attestation Issues
Source: https://developers.circle.com/cctp/howtos/resolve-stuck-attestation
Troubleshoot and resolve common problems with CCTP attestations
This guide helps you resolve issues when a CCTP attestation takes longer than
expected to become available or when the attestation API returns unexpected
responses.
## Understanding attestation timing
After a successful burn transaction, Circle's Attestation Service (Iris) must:
1. Observe the burn event on the source blockchain
2. Wait for sufficient block confirmations
3. Sign the message and make the attestation available through the
[CCTP API](/api-reference/cctp/all/get-messages-v2)
This process takes different amounts of time depending on the transfer type:
| Transfer type | Finality threshold | Typical wait time |
| ----------------- | ------------------ | -------------------------------------------------------------------------------------------------------------- |
| Fast Transfer | ≤ 1000 | Seconds to a few minutes |
| Standard Transfer | ≥ 2000 | Varies by blockchain (see [Finality and Block Confirmations](/cctp/concepts/finality-and-block-confirmations)) |
## Why 404 responses are expected
The attestation API returns a 404 response until the attestation service has
observed and processed your burn transaction. This is expected and does not
indicate an error.
The API returns 404 when:
* The burn transaction hasn't reached the required block confirmations
* The attestation service hasn't yet indexed the transaction
* The transaction hash or domain ID is incorrect
Don't treat 404 as a failure. Instead, implement polling with appropriate
intervals.
## Check attestation status
Query the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint
to check the current status of your attestation.
The following table explains the possible responses and what to do next:
| Response | Meaning | Action |
| -------------------------- | ----------------------------------- | ---------------------------------------------------------- |
| 404 | Attestation not yet observed | Continue polling until the attestation is available |
| `{ "messages": [] }` | Transaction found but not processed | Continue polling until the transaction is processed |
| `{ "status": "pending" }` | Awaiting block confirmations | Continue polling until the block confirmations are reached |
| `{ "status": "complete" }` | Attestation ready | Proceed to mint |
## Implement effective polling
Poll the attestation API at regular intervals without exceeding rate limits:
```ts TypeScript theme={null}
async function waitForAttestation(
sourceDomain: number,
transactionHash: string,
) {
const pollInterval = 5000; // Poll every 5 seconds
const maxWaitTime = 1200000; // 20 minutes maximum
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${sourceDomain}?transactionHash=${transactionHash}`,
);
// 404 is expected while waiting - continue polling
if (response.status === 404) {
console.log("Attestation not yet available (404), waiting...");
await new Promise((resolve) => setTimeout(resolve, pollInterval));
continue;
}
// Rate limited - wait before retrying
if (response.status === 429) {
console.log("Rate limited, waiting 5 minutes...");
await new Promise((resolve) => setTimeout(resolve, 300000));
continue;
}
if (!response.ok) {
throw new Error(`Unexpected HTTP error: ${response.status}`);
}
const data = await response.json();
// Empty messages array - transaction found but not processed
if (!data.messages || data.messages.length === 0) {
console.log("Transaction found, awaiting processing...");
await new Promise((resolve) => setTimeout(resolve, pollInterval));
continue;
}
const message = data.messages[0];
// Attestation complete
if (message.status === "complete" && message.attestation) {
console.log("Attestation retrieved successfully!");
return {
message: message.message,
attestation: message.attestation,
decodedMessage: message.decodedMessage,
};
}
// Still pending
console.log(`Attestation status: ${message.status}`);
await new Promise((resolve) => setTimeout(resolve, pollInterval));
} catch (error) {
console.error("Error fetching attestation:", (error as Error).message);
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}
}
throw new Error("Attestation not received within maximum wait time");
}
```
### Avoid rate limiting
The attestation service limits requests to 35 per second. If you exceed this
limit, the service blocks all API requests for 5 minutes and returns HTTP 429.
Best practices:
* Use a poll interval of at least 5 seconds
* Implement exponential back-off for repeated 429 responses
* Don't poll from multiple clients for the same transaction
## Troubleshooting checklist
If your attestation isn't available after the expected wait time:
Check the source blockchain's block explorer to confirm:
* The transaction succeeded (not reverted)
* The transaction is included in a mined block
* Sufficient blocks have been confirmed since the transaction
Verify you're using the correct:
* Source domain ID (see
[Supported Chains and Domains](/cctp/concepts/supported-chains-and-domains))
* Transaction hash (full hash, including `0x` prefix for EVM chains)
* API environment (sandbox vs. production)
Standard Transfers require full finality. For some blockchains, this can take
significantly longer than Fast Transfers. Check
[Finality and Block Confirmations](/cctp/concepts/finality-and-block-confirmations)
for expected times.
Test that you can reach the API:
```shell Shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/publicKeys' \
--header 'Accept: application/json'
```
If this fails, check your network connectivity and firewall settings.
# Retry a Failed Mint
Source: https://developers.circle.com/cctp/howtos/retry-failed-mint
Complete a CCTP transfer when the mint transaction fails
This guide helps you complete a CCTP transfer when you have a valid attestation
but the mint transaction on the destination blockchain fails or was never
submitted.
## Minting is safe to retry
CCTP minting is idempotent. Each attestation contains a unique nonce that can
only be used once. If you submit the same attestation multiple times, only the
first successful transaction mints USDC. Subsequent attempts revert with a
"nonce already used" error but don't result in duplicate minting.
This means you can safely retry a failed mint without risking double-spending.
## Common mint failure reasons
| Failure reason | Symptoms | Solution |
| ------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Insufficient gas | Transaction reverts or times out | Increase gas limit and retry |
| Nonce already used | Transaction reverts with nonce error | The mint already succeeded; check recipient balance |
| Wrong contract address | Transaction may succeed with no USDC minted | Verify you're using the correct `MessageTransmitterV2` address for the destination blockchain |
| Destination caller restriction | Transaction reverts | Check if the burn specified a `destinationCaller`; only that address can mint |
| Token account doesn't exist (Solana) | Transaction fails | Create the recipient's USDC token account first |
| Attestation expired | Transaction reverts | Use re-attestation API to get a fresh attestation |
## Verify the current state
Before retrying, check whether the mint already succeeded:
Query the recipient's USDC balance on the destination blockchain. If the
expected amount is present, the mint already completed.
Search the destination blockchain's block explorer for `receiveMessage`
transactions from your wallet to the `MessageTransmitterV2` contract.
Query the [attestation API](/api-reference/cctp/all/get-messages-v2) to confirm
you have a `complete` status:
## Retry the mint transaction
If the mint hasn't completed, submit a new `receiveMessage` transaction using
your attestation.
Call `receiveMessage` on the `MessageTransmitterV2` contract:
```ts TypeScript theme={null}
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationData {
message: string;
attestation: string;
}
const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!;
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account,
});
const publicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
// MessageTransmitterV2 contract address - verify for your destination chain
// See: https://developers.circle.com/cctp/references/contract-addresses
const MESSAGE_TRANSMITTER_V2 = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
async function retryMint(data: AttestationData) {
console.log("Retrying mint transaction...");
try {
const txHash = await walletClient.sendTransaction({
to: MESSAGE_TRANSMITTER_V2,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
data.message as `0x${string}`,
data.attestation as `0x${string}`,
],
}),
});
console.log(`Mint transaction submitted: ${txHash}`);
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
});
if (receipt.status === "success") {
console.log("Mint successful!");
return { success: true, txHash };
} else {
console.log("Mint transaction reverted");
return { success: false, txHash };
}
} catch (error) {
// Check if the error indicates nonce already used
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("nonce") || errorMessage.includes("already")) {
console.log(
"Nonce already used - mint may have already completed. Check recipient balance.",
);
}
throw error;
}
}
// Use attestation data from the API
const attestationData: AttestationData = {
message: "0x00000001000000000000001a...", // Full message hex from API
attestation: "0xde09db65dea64090570d8143...", // Full attestation hex from API
};
await retryMint(attestationData);
```
Call `receiveMessage` on the `MessageTransmitterV2` program. For Solana, ensure
the recipient's USDC token account exists before calling `receiveMessage`.
```ts TypeScript theme={null}
import crypto from "crypto";
import {
address,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
getAddressEncoder,
getProgramDerivedAddress,
getSignatureFromTransaction,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
interface AttestationData {
message: string;
attestation: string;
}
// Solana Configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
Uint8Array.from(solanaPrivateKey),
);
// Solana CCTP Program Addresses (Devnet)
const MESSAGE_TRANSMITTER_PROGRAM = address(
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);
async function retryMintOnSolana(
attestationData: AttestationData,
sourceDomain: number,
) {
console.log("Retrying mint on Solana...");
const addressEncoder = getAddressEncoder();
// Derive receiver's USDC token account
const [receiverUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
// Derive required PDAs
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [authorityPda] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter_authority")],
});
// Calculate used nonces PDA
const messageBytes = Buffer.from(attestationData.message.slice(2), "hex");
const nonce = messageBytes.readBigUInt64BE(12);
const firstNonce = (nonce / 6400n) * 6400n;
const firstNonceBuffer = Buffer.alloc(8);
firstNonceBuffer.writeBigUInt64BE(firstNonce);
const sourceDomainBuffer = Buffer.alloc(4);
sourceDomainBuffer.writeUInt32BE(sourceDomain);
const [usedNonces] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [
new TextEncoder().encode("used_nonces"),
sourceDomainBuffer,
firstNonceBuffer,
],
});
// Derive TokenMessengerMinterV2 PDAs
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(sourceDomain.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
const sourceTokenBytes = messageBytes.slice(133, 165);
const [tokenPair] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("token_pair"),
sourceDomainBuffer,
sourceTokenBytes,
],
});
const [custody] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("custody"),
addressEncoder.encode(USDC_MINT),
],
});
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [tokenProgramEventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
// Build instruction
const discriminator = crypto
.createHash("sha256")
.update("global:receive_message")
.digest()
.slice(0, 8);
const messageBuffer = Buffer.from(attestationData.message.slice(2), "hex");
const attestationBuffer = Buffer.from(
attestationData.attestation.slice(2),
"hex",
);
const messageLenBuffer = Buffer.alloc(4);
messageLenBuffer.writeUInt32LE(messageBuffer.length);
const attestationLenBuffer = Buffer.alloc(4);
attestationLenBuffer.writeUInt32LE(attestationBuffer.length);
const instructionData = new Uint8Array(
Buffer.concat([
discriminator,
messageLenBuffer,
messageBuffer,
attestationLenBuffer,
attestationBuffer,
]),
);
const receiveMessageIx = {
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 0 },
{ address: authorityPda, role: 0 },
{ address: messageTransmitter, role: 0 },
{ address: usedNonces, role: 1 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: eventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: tokenMessenger, role: 0 },
{ address: remoteTokenMessenger, role: 0 },
{ address: tokenMinter, role: 1 },
{ address: localToken, role: 1 },
{ address: tokenPair, role: 0 },
{ address: receiverUsdcAccount, role: 1 },
{ address: custody, role: 1 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: tokenProgramEventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(receiveMessageIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
try {
await sendAndConfirmTransaction(
signedTransaction as Parameters[0],
{
commitment: "confirmed",
},
);
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Mint successful! Signature: ${signature}`);
return signature;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("already been processed")) {
console.log("Nonce already used - mint may have already completed.");
}
throw error;
}
}
// Use attestation data from the API
const attestationData: AttestationData = {
message: "0x000000000000000500000000...", // Full message hex from API
attestation: "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d43...", // Full attestation hex from API
};
await retryMintOnSolana(attestationData, 0); // 0 = Ethereum Sepolia domain
```
**Note:** The recipient's USDC token account must exist before calling
`receiveMessage`. If the account doesn't exist, create it using the Associated
Token Program before retrying the mint.
## Handle destination caller restrictions
If the burn specified a `destinationCaller` address, only that address can call
`receiveMessage`. If you're seeing authorization errors:
1. Check the `destinationCaller` field in the attestation's `decodedMessage`
2. If it's not `0x0000...0000`, ensure you're calling from the specified address
# Transfer USDC from Arbitrum to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore
This guide shows how to transfer USDC from Arbitrum to HyperCore using the
`CctpExtension` contract.
Fast Transfers from Arbitrum to HyperEVM have no fees, however there is a flat
forwarding fee for Arbitrum transfers to HyperCore. Fast Transfer is the default
for transfers from Arbitrum to HyperEVM.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM testnet wallet with the private key available
* Added Arbitrum Sepolia network to your wallet
([network details](https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers))
* Funded your wallet with the following testnet tokens:
* Arbitrum Sepolia ETH (native token) from a
[public faucet](https://faucet.quicknode.com/arbitrum/sepolia)
* Arbitrum Sepolia USDC from the [Circle Faucet](https://faucet.circle.com)
* Created a new Node project and installed dependencies:
```bash theme={null}
npm install viem
npm install -D tsx typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
FORWARD_RECIPIENT=0x... # Your HyperCore address to receive the USDC
```
## Steps
Use the following steps to transfer USDC from Arbitrum to HyperCore.
### Step 1. Get CCTP fees from the API
Query the CCTP API for the fees for transferring USDC from Arbitrum to
HyperCore. This value is passed to the `maxFee` parameter in the
`batchDepositForBurnWithAuth` transaction. The following is an example request
to the CCTP using source domain 3 (Arbitrum) and destination domain 19
(HyperEVM):
```shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/3/19?forward=true&hyperCoreDeposit=true' \
--header 'Content-Type: application/json'
```
**Response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // fast transfer
"minimumFee": 0, // no protocol fee
"forwardFee": {
"low": 200000, // 0.20 USDC
"med": 200000, // low, med, high will be the same static fee
"high": 200000
}
},
{
"finalityThreshold": 2000, // standard transfer
"minimumFee": 0,
"forwardFee": {
"low": 200000,
"med": 200000,
"high": 200000
}
}
]
```
### Step 2. Calculate the USDC amounts minus fees
There is no fee to deposit USDC from Arbitrum to HyperEVM, but there is a flat
forwarding fee for the transfer to HyperCore. The forwarding fee is 0.20 USDC
(`0_200_000` subunits). For a 10 USDC transfer from Arbitrum to HyperCore, the
total fee is 0.20 USDC.
The forwarding fee is deducted from your transfer amount. For a 10 USDC
transfer, you will receive 9.80 USDC on HyperCore.
### Step 3. Sign a `ReceiveWithAuthorization` transaction on the USDC contract
Create a `ReceiveWithAuthorization` transaction for the USDC contract with the
following parameters:
* `from`: Your wallet address
* `to`: The `CctpExtension` contract address
* `value`: The amount of USDC to transfer
* `validAfter`: The timestamp after which the transaction is valid
* `validBefore`: The timestamp before which the transaction is valid
* `nonce`: A random nonce
Sign the hash of the transaction with your private key, and derive the `v`, `r`,
`s` values. Broadcast the transaction to the blockchain.
### Step 4. Sign and broadcast a `batchDepositForBurnWithAuth` transaction on the `CctpExtension` contract
Create a `batchDepositForBurnWithAuth` transaction for the `CctpExtension`
contract with the following parameters:
* `destinationDomain`: 19 (HyperEVM)
* `mintRecipient`: The `CctpForwarder` contract address on HyperEVM
* `destinationCaller`: The `CctpForwarder` contract address on HyperEVM
* `maxFee`: `0_200_000` (0.20 USDC, from step 2)
* `minFinalityThreshold`: `1000` (Fast Transfer)
* `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM
Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM
when you transfer USDC to HyperCore.
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
The `hookData` is the data to execute the forwarder to HyperCore. The following
is an example of the hook data:
```ts TypeScript theme={null}
/**
* Generate CCTP forwarder hook data for HyperCore
*
* Hook Data Format:
* Field Bytes Type Index
* magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding
* version 4 uint32 24
* dataLength 4 uint32 28
* hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore
* hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot)
*/
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
// Validate hex prefix if recipient provided
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
// Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
// Version: uint32 = 0 (4 bytes, big-endian)
const version = "00000000";
if (!hyperCoreMintRecipient) {
// No recipient: dataLength = 0, return header only (32 bytes)
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
// With recipient: dataLength = 24 (20 bytes address + 4 bytes dex)
const dataLength = "00000018"; // 24 in hex
// Address: 20 bytes (remove 0x prefix)
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
// Destination DEX: uint32 big-endian
// 0 = perps, 4294967295 (0xFFFFFFFF) = spot
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
```
Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and
automatically forwarded to your address on HyperCore.
By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps
balance on HyperCore. To deposit to the spot balance, set
`hyperCoreDestinationDex` to `4294967295` (uint32 max value).
## Full example code
The following is a complete example of how to transfer USDC from Arbitrum to
HyperCore.
```ts script.ts expandable theme={null}
/**
* Script: Call CctpExtension.batchDepositForBurnWithAuth
* - Generates EIP-3009 receiveWithAuthorization signature
* - Executes a CCTP burn via the extension
* - Supports Forwarder hook data to auto-forward to HyperCore
*/
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
// -------- Contract ABIs --------
const CCTP_EXTENSION_ABI = [
{
name: "batchDepositForBurnWithAuth",
type: "function",
stateMutability: "nonpayable",
inputs: [
{
name: "_receiveWithAuthorizationData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "authValidAfter", type: "uint256" },
{ name: "authValidBefore", type: "uint256" },
{ name: "authNonce", type: "bytes32" },
{ name: "v", type: "uint8" },
{ name: "r", type: "bytes32" },
{ name: "s", type: "bytes32" },
],
},
{
name: "_depositForBurnData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
},
],
outputs: [],
},
] as const;
// -------- Configuration --------
const config = {
privateKey: (process.env.PRIVATE_KEY || "0x") as Hex,
// Contract addresses (Arbitrum Sepolia Testnet)
cctpExtension: "0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D" as Address,
usdcToken: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d" as Address,
// Transfer parameters
amount: "2", // USDC amount to transfer
maxFee: "0.2", // Max fee in USDC
// CCTP parameters
destinationDomain: 19, // HyperEVM domain
cctpForwarder: "0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2" as Address, // HyperEVM testnet
// HyperCore recipient
forwardRecipient: process.env.FORWARD_RECIPIENT as Address,
destinationDex: 0, // 0 = perps, 4294967295 = spot
// EIP-3009 validity window (seconds)
validAfter: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
validBefore: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
};
// -------- Generate Hook Data --------
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
const version = "00000000";
if (!hyperCoreMintRecipient) {
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
const dataLength = "00000018";
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
// -------- Generate Random Nonce --------
function generateNonce(): Hex {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
return `0x${Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
}
// -------- Main Function --------
async function main() {
// Validate private key and recipient
if (!config.privateKey || config.privateKey === "0x") {
throw new Error("Set PRIVATE_KEY");
}
if (!config.forwardRecipient) {
throw new Error("Set FORWARD_RECIPIENT");
}
// Setup account and clients
const account = privateKeyToAccount(config.privateKey);
const publicClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: arbitrumSepolia,
transport: http(),
account,
});
const amount = parseUnits(config.amount, 6);
const maxFee = parseUnits(config.maxFee, 6);
console.log("User:", account.address);
console.log("Extension:", config.cctpExtension);
console.log("USDC:", config.usdcToken);
console.log("Total (USDC):", config.amount);
console.log(
"Dest Domain:",
config.destinationDomain,
"\nMint Recipient:",
config.cctpForwarder,
);
console.log("Max Fee (USDC):", config.maxFee, "\nMin Finality:", 1000);
// Check USDC balance
const balance = await publicClient.readContract({
address: config.usdcToken,
abi: [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
],
functionName: "balanceOf",
args: [account.address],
});
if (balance < amount) {
throw new Error(
`Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`,
);
}
// Generate hook data
const hookData = encodeForwardHookData(
config.forwardRecipient,
config.destinationDex,
);
console.log(
"Forwarder hook enabled -> Final recipient:",
config.forwardRecipient,
);
console.log("Hook Data:", hookData);
// Convert addresses to bytes32
const mintRecipientBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
const destinationCallerBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
console.log("Destination Caller (bytes32):", destinationCallerBytes32);
// Generate nonce for EIP-3009
const nonce = generateNonce();
// Sign EIP-3009 ReceiveWithAuthorization
const signature = await walletClient.signTypedData({
domain: {
name: "USD Coin",
version: "2",
chainId: arbitrumSepolia.id,
verifyingContract: config.usdcToken,
},
types: {
ReceiveWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "ReceiveWithAuthorization",
message: {
from: account.address,
to: config.cctpExtension,
value: amount,
validAfter: BigInt(config.validAfter),
validBefore: BigInt(config.validBefore),
nonce,
},
});
// Parse signature into v, r, s
const r = signature.slice(0, 66) as Hex;
const s = `0x${signature.slice(66, 130)}` as Hex;
const v = parseInt(signature.slice(130, 132), 16);
// Estimate gas
const gasEstimate = await publicClient.estimateContractGas({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
account,
});
console.log("Estimated gas:", gasEstimate.toString());
// Execute batchDepositForBurnWithAuth
const hash = await walletClient.writeContract({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
gas: (gasEstimate * 120n) / 100n, // +20%
});
console.log("Tx hash:", hash);
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED");
console.log(
"Block:",
receipt.blockNumber,
"\nGas Used:",
receipt.gasUsed.toString(),
);
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
```
Run the script:
```bash theme={null}
npx tsx --env-file=.env script.ts
```
# Transfer USDC from Ethereum to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-ethereum-to-hypercore
This guide shows the steps to transfer USDC from Ethereum to HyperCore using the
`TokenMessengerV2` contract with hook data to call the `CctpForwarder` contract
on HyperEVM. This CCTP flow follows the same pattern as USDC transfers from
Ethereum to any other domain, except for the inclusion of hook data to call the
`CctpForwarder` contract on HyperEVM.
While this guide uses Ethereum as an example, the same steps apply to any EVM
chain that supports CCTP via `TokenMessengerV2`. Adjust the source domain ID and
contract addresses for your chain.
This guide does not provide full example code, you can find an example of
transfers from Ethereum in the
[CCTP quickstart](cctp/quickstarts/transfer-usdc-ethereum-to-arc).
Fast Transfers from Ethereum to HyperEVM incur a protocol fee and a dynamic
forwarding fee for the HyperEVM chain relay transaction. Fast Transfer is the
default for transfers from Ethereum to HyperCore.
## Steps
Use the following steps to transfer USDC from Ethereum to HyperCore.
### Step 1. Get CCTP fees from the API
Query the CCTP API for the fees for transferring USDC from Ethereum to
HyperCore. This value is passed to the `maxFee` parameter in the
`depositForBurnWithHook` transaction. The following is an example request to the
CCTP using source domain 0 (Ethereum) and destination domain 19 (HyperEVM):
```shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/0/19?forward=true&hyperCoreDeposit=true' \
--header 'Content-Type: application/json'
```
**Response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // fast transfer
"minimumFee": 1, // in basis points
"forwardFee": {
"low": 211203,
"med": 216109, // 0.216109 USDC
"high": 221014
}
},
{
"finalityThreshold": 2000, // standard transfer
"minimumFee": 0,
"forwardFee": {
"low": 211203,
"med": 216109, // 0.216109 USDC
"high": 221014
}
}
]
```
### Step 2. Calculate the USDC amounts minus fees
There is a protocol fee to deposit USDC from Ethereum to HyperEVM and a dynamic
forwarding fee for the HyperEVM chain relay transaction. The CCTP fast transfer
fee is 1 basis point (0.01%) of the transfer amount. The forwarding fee is 0.20
USDC (`0_200_000` subunits) plus a dynamic destination chain gas fee. For a 10
USDC transfer from Ethereum to HyperCore, the protocol fee is 0.001 USDC (10
USDC × 0.0001) and an example forwarding fee is 0.216109 USDC, for a total fee
of 0.217109 USDC.
Because the protocol fee scales with the transfer amount and the forwarding fee
is dynamic, you must recalculate `maxFee` for each transfer. For a programmatic
approach, see [`calculateMaxFee`](/cctp/concepts/fees#maximum-fee-parameter) on
the fees page.
### Step 3. Approve the USDC transfer
To allow the `TokenMessengerV2` contract to transfer the USDC on your behalf,
you need to approve the transfer. This is done by calling the `approve` function
on the USDC contract.
You can see an example of this contract call in the
[Ethereum CCTP V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/evm.ts#L63).
### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract
Create a `depositForBurnWithHook` transaction for the `TokenMessengerV2`
contract with the following parameters:
* `amount`: The amount of USDC to transfer
* `destinationDomain`: 19 (HyperEVM)
* `mintRecipient`: The address of the `CctpForwarder` contract on HyperEVM
* `burnToken`: The address of the USDC contract on the source chain
* `destinationCaller`: The address of the `CctpForwarder` contract on HyperEVM
* `maxFee`: The protocol fee + forwarding fee calculated in Step 2
* `minFinalityThreshold`: `1000` (Fast Transfer)
* `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM
Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM
when you transfer USDC to HyperCore.
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
The `hookData` is the data to execute the forwarder to HyperCore. The following
is an example of the hook data:
```ts TypeScript theme={null}
/**
* Generate CCTP forwarder hook data for HyperCore
*
* Hook Data Format:
* Field Bytes Type Index
* magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding
* version 4 uint32 24
* dataLength 4 uint32 28
* hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore
* hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot)
*/
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
// Validate hex prefix if recipient provided
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
// Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
// Version: uint32 = 0 (4 bytes, big-endian)
const version = "00000000";
if (!hyperCoreMintRecipient) {
// No recipient: dataLength = 0, return header only (32 bytes)
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
// With recipient: dataLength = 24 (20 bytes address + 4 bytes dex)
const dataLength = "00000018"; // 24 in hex
// Address: 20 bytes (remove 0x prefix)
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
// Destination DEX: uint32 big-endian
// 0 = perps, 4294967295 (0xFFFFFFFF) = spot
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
```
You can see an example of this contract call in the
[Ethereum V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/evm.ts#L105)
Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and
automatically forwarded to your address on HyperCore.
By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps
balance on HyperCore. To deposit to the spot balance, set
`hyperCoreDestinationDex` to `4294967295` (uint32 max value).
# Transfer USDC from HyperEVM to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-hyperevm-to-hypercore
This guide shows how to transfer USDC from HyperEVM to HyperCore using the
`CoreDepositWallet` contract.
**Tip:** The `CoreDepositWallet` contract provides `deposit`, `depositFor`,
and `depositWithAuth` methods. This guide uses `deposit`. All methods accept a
`destinationDex` parameter (`0` for perps, `4294967295` for spot). See the
[CoreDepositWallet contract
interface](/cctp/references/coredepositwallet-contract-interface) for detailed
information.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM testnet wallet with the private key available
* Funded your wallet with HyperEVM testnet USDC from the
[Circle Faucet](https://faucet.circle.com)
* Created a new Node project and installed dependencies:
```bash theme={null}
npm install viem
npm install -D tsx typescript @types/node
```
* Created a `.env` file with required environment variable:
```text theme={null}
PRIVATE_KEY=0x...
```
## Steps
Use the following steps to transfer USDC from HyperEVM to HyperCore.
### Step 1. Approve the `CoreDepositWallet` to spend USDC
Approve the `CoreDepositWallet` contract to transfer USDC on your behalf:
```ts TypeScript theme={null}
const hash = await walletClient.writeContract({
address: USDC_ADDRESS,
abi: USDC_ABI,
functionName: "approve",
args: [CORE_DEPOSIT_WALLET, amount],
});
await publicClient.waitForTransactionReceipt({ hash });
```
### Step 2. Call the `deposit` function
Call the `deposit` function with your desired amount and destination:
```ts TypeScript theme={null}
const hash = await walletClient.writeContract({
address: CORE_DEPOSIT_WALLET,
abi: CORE_DEPOSIT_WALLET_ABI,
functionName: "deposit",
args: [amount, destinationDex], // 0 = perps, 4294967295 = spot
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
```
The `deposit` function transfers USDC from your account to the
`CoreDepositWallet` and credits your HyperCore balance.
## Full example code
The following is a complete example of how to transfer USDC from HyperEVM to
HyperCore.
```ts script.ts expandable theme={null}
/**
* Script: Call CoreDepositWallet.deposit on HyperEVM
* - Approves USDC spending
* - Calls deposit(amount, destinationDex)
*/
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hyperliquidEvmTestnet } from "viem/chains";
// -------- Configuration --------
const config = {
privateKey: (process.env.PRIVATE_KEY || "0x") as Hex,
// Contract addresses (HyperEVM Testnet)
coreDepositWallet: "0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206" as Address,
usdcToken: "0x2B3370eE501B4a559b57D449569354196457D8Ab" as Address,
// Transfer parameters
amount: "2", // USDC amount to deposit
// HyperCore destination (0 = perps, 4294967295 = spot)
destinationDex: 0,
};
// -------- Main Function --------
async function main() {
// Validate private key
if (!config.privateKey || config.privateKey === "0x") {
throw new Error("Set PRIVATE_KEY");
}
// Setup account and clients
const account = privateKeyToAccount(config.privateKey);
const publicClient = createPublicClient({
chain: hyperliquidEvmTestnet,
transport: http(),
});
const walletClient = createWalletClient({
chain: hyperliquidEvmTestnet,
transport: http(),
account,
});
const amount = parseUnits(config.amount, 6);
console.log("User:", account.address);
console.log("CoreDepositWallet:", config.coreDepositWallet);
console.log("USDC:", config.usdcToken);
console.log("Amount (USDC):", config.amount);
console.log(
"Destination DEX:",
config.destinationDex === 0 ? "perps" : "spot",
);
// Check USDC balance
const balance = await publicClient.readContract({
address: config.usdcToken,
abi: [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
],
functionName: "balanceOf",
args: [account.address],
});
if (balance < amount) {
throw new Error(
`Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`,
);
}
// Check current allowance
const currentAllowance = await publicClient.readContract({
address: config.usdcToken,
abi: [
{
name: "allowance",
type: "function",
stateMutability: "view",
inputs: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
],
outputs: [{ name: "", type: "uint256" }],
},
],
functionName: "allowance",
args: [account.address, config.coreDepositWallet],
});
// Step 1: Approve if needed
if (currentAllowance < amount) {
console.log("\nApproving USDC spending...");
const hash = await walletClient.writeContract({
address: config.usdcToken,
abi: [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [config.coreDepositWallet, amount],
});
console.log("Approve tx hash:", hash);
await publicClient.waitForTransactionReceipt({ hash });
console.log("Approval confirmed");
} else {
console.log("\nSufficient allowance already exists");
}
// Step 2: Deposit
console.log("\nDepositing USDC to HyperCore...");
const hash = await walletClient.writeContract({
address: config.coreDepositWallet,
abi: [
{
name: "deposit",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDex", type: "uint32" },
],
outputs: [],
},
],
functionName: "deposit",
args: [amount, config.destinationDex],
});
console.log("Deposit tx hash:", hash);
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED");
console.log(
"Block:",
receipt.blockNumber,
"\nGas Used:",
receipt.gasUsed.toString(),
);
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
```
Run the script:
```bash theme={null}
npx tsx --env-file=.env script.ts
```
# Transfer USDC from Solana to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-solana-to-hypercore
This guide shows how to transfer USDC from Solana to HyperCore using the
`TokenMessengerV2` contract. Solana's CCTP implementation does not have the
`depositForBurnWithAuth`, and there is no `CctpExtension` contract for Solana.
As such, transfers from Solana to HyperCore follow the standard CCTP flow, with
the addition of hook data to call the `CctpForwarder` contract on HyperEVM.
This guide does not provide full example code for the transfer to HyperCore from
Solana.
Fast Transfers from Solana to HyperEVM incur a protocol fee and a dynamic
forwarding fee for the HyperEVM chain relay transaction. Fast Transfer is the
default for transfers from Solana to HyperCore.
## Steps
Use the following steps to transfer USDC from Solana to HyperCore.
### Step 1. Get CCTP fees from the API
Query the CCTP API for the fees for transferring USDC from Solana to HyperCore.
This value is passed to the `maxFee` parameter in the `depositForBurnWithHook`
transaction. The following is an example request to the CCTP using source domain
5 (Solana) and destination domain 19 (HyperEVM):
```shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/19?forward=true&hyperCoreDeposit=true' \
--header 'Content-Type: application/json'
```
**Response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // fast transfer
"minimumFee": 1, // in basis points
"forwardFee": {
"low": 211203,
"med": 216109, // 0.216109 USDC
"high": 221014
}
},
{
"finalityThreshold": 2000, // standard transfer
"minimumFee": 0,
"forwardFee": {
"low": 211203,
"med": 216109, // 0.216109 USDC
"high": 221014
}
}
]
```
### Step 2. Calculate the USDC amounts minus fees
There is a protocol fee to deposit USDC from Solana to HyperEVM and a dynamic
forwarding fee for the HyperEVM chain relay transaction. The CCTP fast transfer
fee is 1 basis point (0.01%) of the transfer amount. The forwarding fee is 0.20
USDC (`0_200_000` subunits) plus a dynamic destination chain gas fee. For a 10
USDC transfer from Solana to HyperCore, the protocol fee is 0.001 USDC (10 USDC
× 0.0001) and an example forwarding fee is 0.216109 USDC, for a total fee of
0.217109 USDC.
Because the protocol fee scales with the transfer amount and the forwarding fee
is dynamic, you must recalculate `maxFee` for each transfer. For a programmatic
approach, see [`calculateMaxFee`](/cctp/concepts/fees#maximum-fee-parameter) on
the fees page.
### Step 3. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract
Create a `depositForBurnWithHook` transaction for the `TokenMessengerV2`
contract with the following parameters:
* `amount`: The amount of USDC to transfer
* `destinationDomain`: 19 (HyperEVM)
* `mintRecipient`: The address of the `CctpForwarder` contract on HyperEVM
* `destinationCaller`: The address of the `CctpForwarder` contract on HyperEVM
* `maxFee`: The protocol fee + forwarding fee calculated in Step 2
* `minFinalityThreshold`: `1000` (Fast Transfer)
* `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM
Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM
when you transfer USDC to HyperCore.
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
The `hookData` is the data to execute the forwarder to HyperCore. The following
is an example of the hook data:
```ts TypeScript theme={null}
/**
* Generate CCTP forwarder hook data for HyperCore
*
* Hook Data Format:
* Field Bytes Type Index
* magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding
* version 4 uint32 24
* dataLength 4 uint32 28
* hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore
* hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot)
*/
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
// Validate hex prefix if recipient provided
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
// Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
// Version: uint32 = 0 (4 bytes, big-endian)
const version = "00000000";
if (!hyperCoreMintRecipient) {
// No recipient: dataLength = 0, return header only (32 bytes)
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
// With recipient: dataLength = 24 (20 bytes address + 4 bytes dex)
const dataLength = "00000018"; // 24 in hex
// Address: 20 bytes (remove 0x prefix)
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
// Destination DEX: uint32 big-endian
// 0 = perps, 4294967295 (0xFFFFFFFF) = spot
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
```
For full example code calling the `depositForBurnWithHook` function, see the
[Solana CCTP V2 example on GitHub](https://github.com/circlefin/solana-cctp-contracts/blob/9f8cf26d059cf8927ae0a0b351f3a7a88c7bdade/examples/v2/solana.ts#L94).
Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and
automatically forwarded to your address on HyperCore.
By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps
balance on HyperCore. To deposit to the spot balance, set
`hyperCoreDestinationDex` to `4294967295` (uint32 max value).
# How-to: Transfer USDC with the Forwarding Service
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-with-forwarding-service
Transfer USDC Crosschain with the Circle Forwarding Service
This guide shows how to transfer USDC crosschain using the
[Circle Forwarding Service](/cctp/concepts/forwarding-service). This example
shows a transfer from Base Sepolia to Avalanche Fuji, but you can use the same
steps to transfer to any supported
[destination blockchain](/cctp/concepts/supported-chains-and-domains), including
Solana.
When you use the Forwarding Service, Circle handles the mint transaction on the
destination blockchain, eliminating the need for you to hold native tokens for
gas on the destination blockchain or run multichain infrastructure.
## Prerequisites
Before you start, ensure you have:
* Installed [Node.js v22+](https://nodejs.org/)
* Created a TypeScript project and installed the `viem` package.
* Created a wallet with the private key available on the source chain.
* Funded the wallet with testnet USDC and native tokens for gas fees on the
source chain.
* Created a `.env` file with your private key and recipient address.
## Steps
Use the following steps to transfer USDC with the Forwarding Service.
### Step 1. Get CCTP fees from the API
Query the CCTP API for the fees for transferring USDC from Base Sepolia to
Avalanche Fuji. This value is passed to the `maxFee` parameter in the
`depositForBurnWithHook` transaction. The following is an example request using
source domain 6 (Base Sepolia) and destination domain 1 (Avalanche Fuji):
```typescript theme={null}
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/1?forward=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
const fees = await response.json();
console.log(fees);
```
**Example response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // Fast transfer
"minimumFee": 1.3, // Basis points (0.013% fee rate)
"forwardFee": {
// Gas-based, fluctuates based on destination chain gas prices
"low": 206035, // 0.206035 USDC
"med": 207543, // 0.207543 USDC
"high": 209052 // 0.209052 USDC
}
},
{
"finalityThreshold": 2000, // Standard transfer
"minimumFee": 0, // No fee
"forwardFee": {
"low": 206035, // 0.206035 USDC
"med": 207543, // 0.207543 USDC
"high": 209052 // 0.209052 USDC
}
}
]
```
The `forwardFee` is the fee charged by the Forwarding Service. The `minimumFee`
is the CCTP protocol fee rate in basis points, applied as a percentage of the
transfer amount.
Circle recommends selecting the `med` fee level or higher from the `forwardFee`
object in the API response. Note that `forwardFee` values fluctuate based on
destination chain gas prices. Make the query immediately before initiating your
transfer.
When the destination blockchain is Solana, the `forwardFee` values include both
gas and rent costs. If the recipient does not have an existing USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account),
add `includeRecipientSetup=true` to the fee query so the returned fee covers ATA
creation:
```typescript theme={null}
// Source domain 6 (Base Sepolia), destination domain 5 (Solana)
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/5?forward=true&includeRecipientSetup=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
```
Unlike EVM destinations, the `mintRecipient` for Solana must be the recipient's
**USDC token account address** (ATA), not the wallet address. Derive the ATA
from the wallet address and the USDC mint:
```typescript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet
const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet);
const mintRecipientBytes32 =
`0x${Buffer.from(recipientAta.toBytes()).toString("hex")}` as `0x${string}`;
```
If the recipient does not have an existing ATA and you passed
`includeRecipientSetup=true` in the fee query, you must also encode ATA creation
fields in the hook data. See
[Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation)
for the extended hook data format.
### Step 2. Calculate the USDC amounts and fees
Calculate the total fee by combining the protocol fee and the Forwarding Service
fee. The `maxFee` parameter must cover both fees for the transfer to succeed.
```typescript theme={null}
// Amount to transfer (10 USDC in subunits)
const transferAmount = 10_000_000n;
// Parse fees from API response
const feeData = fees[0]; // Use fast transfer fees (finalityThreshold: 1000)
const forwardFee = BigInt(feeData.forwardFee.med);
// Calculate protocol fee (minimumFee is in basis points)
const minimumFeeBps = feeData.minimumFee;
const protocolFee =
(transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;
// Total max fee should cover both fees
const maxFee = forwardFee + protocolFee;
const totalAmount = transferAmount + maxFee; // Total to burn
console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
```
In this example, for a 10 USDC transfer with forwarding, the total fee is
0.208843 USDC (0.207543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol
fee). For the recipient to receive 10 USDC, you must burn 10.208843 USDC in
total.
If the `maxFee` parameter is insufficient to cover the both Fast Transfer
protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding
execution over Fast Transfer. This means that the transfer will execute as a
Standard Transfer with the Forwarding Service.
### Step 3. Approve the USDC transfer
Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on
Base to transfer USDC from your wallet. Approve at least `totalAmount`
(including fees) calculated in Step 2.
```typescript theme={null}
import { createWalletClient, http, encodeFunctionData } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
"0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
// Set up wallet client
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
chain: baseSepolia,
transport: http(),
account,
});
async function approveUSDC(amount: bigint) {
console.log("Approving USDC transfer...");
const approveTx = await client.sendTransaction({
to: BASE_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [BASE_SEPOLIA_TOKEN_MESSENGER, amount],
}),
});
console.log("USDC Approval Tx:", approveTx);
return approveTx;
}
// Approve the total amount (from Step 2)
await approveUSDC(totalAmount);
```
### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract
Create and send a `depositForBurnWithHook` transaction with the Forwarding
Service hook data. The hook data tells the CCTP Forwarding Service to
automatically forward the mint transaction on the destination chain.
The Forwarding Service hook data is a static 32-byte value containing the magic
bytes `cctp-forward`, version `0`, and length `0`. For details on the hook
format, see
[Forwarding Service hook format](/cctp/concepts/forwarding-service#hook-format).
```typescript theme={null}
// Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0)
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
```
When forwarding to Solana, use the same static hook data if the recipient
already has a USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account).
If the recipient does not have an ATA and you included
`includeRecipientSetup=true` in the fee query (see Step 1), construct extended
hook data that requests ATA creation:
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
// Magic bytes "cctp-forward" padded to 24 bytes
const magicBytes = Buffer.alloc(24);
magicBytes.write("cctp-forward", "utf-8");
// Version (uint32, big-endian) = 0
const version = Buffer.alloc(4);
// Length of additional Circle hook data (uint32, big-endian) = 33
const length = Buffer.alloc(4);
length.writeUInt32BE(33);
// ATA creation flag = 1
const ataFlag = Buffer.from([1]);
// Recipient wallet address (32 bytes)
const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
const walletBytes = Buffer.from(recipientWallet.toBytes());
const FORWARDING_SERVICE_HOOK_DATA =
("0x" +
Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString(
"hex",
)) as `0x${string}`;
```
For the full hook data format, see
[Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation).
Then, send the `depositForBurnWithHook` transaction:
Use `totalAmount` (transfer amount + fees) for the `amount` parameter. The
recipient receives only the transfer amount after fees are deducted.
```typescript theme={null}
import { pad, encodeFunctionData } from "viem";
// Configuration
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS" as `0x${string}`;
// Convert address to bytes32 format
const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 });
async function depositForBurnWithHook() {
console.log("Burning USDC on Base with Forwarding Service hook...");
const burnTx = await client.sendTransaction({
to: BASE_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
totalAmount, // Total to burn (recipient receives transferAmount after fees)
AVALANCHE_FUJI_DOMAIN,
mintRecipientBytes32,
BASE_SEPOLIA_USDC,
pad("0x", { size: 32 }), // destinationCaller (empty = any caller)
maxFee,
1000,
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log("Burn Tx:", burnTx);
return burnTx;
}
```
Once the burn transaction is confirmed on Base, the Circle Forwarding Service
automatically handles the attestation and mint transaction on Avalanche. The
USDC is minted directly to the `mintRecipient` address on the destination chain.
The recipient receives `transferAmount` USDC (fees are automatically deducted
from the `totalAmount` on the destination chain).
### Step 5. Verify the mint transaction
After the burn transaction is confirmed, query the Circle Iris API to retrieve
the forwarding details. The API returns the `forwardTxHash`, which is the mint
transaction hash on the destination chain.
The attestation may take time to become available, depending on the destination
chain. Poll the API until the message is ready:
```typescript theme={null}
// Configuration
const BASE_SEPOLIA_DOMAIN = 6;
process.stdout.write("Waiting for attestation...");
let mintTx;
while (!mintTx) {
const messageResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
);
const data = await messageResponse.json();
if (data.messages?.[0]?.forwardTxHash) {
mintTx = data.messages[0].forwardTxHash;
console.log(); // New line after dots
} else {
process.stdout.write(".");
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
console.log("Mint Tx:", mintTx);
```
## Full example code
The following is a complete example of how to transfer USDC from Base Sepolia to
Avalanche Fuji using the Forwarding Service. Remember to set the `PRIVATE_KEY`
and `DESTINATION_ADDRESS` environment variables.
```typescript script.ts expandable theme={null}
import { createWalletClient, http, encodeFunctionData, pad } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// Validate environment variables
if (!process.env.PRIVATE_KEY || !process.env.DESTINATION_ADDRESS) {
throw new Error(
"PRIVATE_KEY and DESTINATION_ADDRESS environment variables are required",
);
}
// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
"0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const BASE_SEPOLIA_DOMAIN = 6;
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = process.env.DESTINATION_ADDRESS as `0x${string}`;
// Forwarding Service hook data
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`;
// Set up wallet client
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
chain: baseSepolia,
transport: http(),
account,
});
async function main() {
console.log("Wallet address:", account.address);
console.log("Destination address:", DESTINATION_ADDRESS);
// Step 1: Get fees from API
console.log("\nStep 1: Getting CCTP fees...");
const feeResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${BASE_SEPOLIA_DOMAIN}/${AVALANCHE_FUJI_DOMAIN}?forward=true`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
const fees = await feeResponse.json();
console.log("Fees:", JSON.stringify(fees, null, 2));
// Step 2: Calculate amounts
console.log("\nStep 2: Calculating amounts...");
const transferAmount = 10_000_000n; // 10 USDC
const feeData = fees[0]; // Fast transfer
const forwardFee = BigInt(feeData.forwardFee.med);
const minimumFeeBps = feeData.minimumFee;
const protocolFee =
(transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = transferAmount + maxFee; // Total to burn
console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
// Step 3: Approve USDC
console.log("\nStep 3: Approving USDC transfer...");
const approveTx = await client.sendTransaction({
to: BASE_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [BASE_SEPOLIA_TOKEN_MESSENGER, totalAmount],
}),
});
console.log("Approval Tx:", approveTx);
// Step 4: Burn USDC with Forwarding Service hook
console.log("\nStep 4: Burning USDC with Forwarding Service hook...");
const burnTx = await client.sendTransaction({
to: BASE_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
totalAmount,
AVALANCHE_FUJI_DOMAIN,
pad(DESTINATION_ADDRESS as `0x${string}`, { size: 32 }),
BASE_SEPOLIA_USDC,
pad("0x", { size: 32 }),
maxFee,
1000, // Fast Transfer
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log("Burn Tx:", burnTx);
console.log(
"\nTransfer initiated. The Forwarding Service will automatically mint USDC on Avalanche.",
);
// Step 5: Verify the mint transaction
console.log("\nStep 5: Verifying mint transaction...");
process.stdout.write("Waiting for attestation...");
let mintTx;
while (!mintTx) {
const messageResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
);
const data = await messageResponse.json();
if (data.messages?.[0]?.forwardTxHash) {
mintTx = data.messages[0].forwardTxHash;
console.log("\n"); // New line after dots
} else {
process.stdout.write(".");
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
console.log("Mint Tx:", mintTx);
}
main().catch(console.error);
```
Run the script:
```bash theme={null}
npx tsx script.ts
```
# Troubleshoot CCTP Transfers
Source: https://developers.circle.com/cctp/howtos/troubleshoot-transfers
Diagnose and resolve stuck or failed crosschain USDC transfers
This guide helps you diagnose and resolve issues with CCTP transfers that appear
stuck or fail to complete. A CCTP transfer involves three stages, and problems
can occur at any point in the process.
## Transfer stages
A CCTP transfer consists of three stages:
1. **Burn**: USDC is burned on the source blockchain
2. **Attestation**: Circle's Attestation Service observes the burn and signs a
message
3. **Mint**: The signed attestation is submitted to mint USDC on the destination
blockchain
If your transfer appears stuck, first identify which stage has the issue.
## Identify where your transfer is stuck
Use the following steps to determine the current state of your transfer:
Verify the burn transaction succeeded on the source blockchain using a block
explorer. If the transaction failed or is pending, the issue is at the burn
stage.
Call the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint
with your transaction hash.
Interpret the response:
* **404 response**: The attestation service hasn't observed the burn yet. This
is normal and expected. See
[Why 404 responses are expected](/cctp/howtos/resolve-stuck-attestation#why-404-responses-are-expected).
* **Empty `messages` array**: The burn exists but hasn't been processed yet.
* **Status `pending`**: The burn is awaiting block confirmations.
* **Status `complete` with attestation**: The attestation is ready. If your
transfer is stuck, the issue is at the mint stage.
If you have an attestation but your destination wallet doesn't have the USDC,
either:
* The mint transaction was never submitted
* The mint transaction failed
Check your destination blockchain for any failed `receiveMessage` transactions.
## Common issues and solutions
| Issue | Cause | Solution |
| ------------------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------- |
| 404 persists for extended time | Burn transaction may have failed or is still confirming | Verify burn succeeded on block explorer, if it failed, retry the burn |
| Attestation status remains `pending` | Waiting for block confirmations | Wait for sufficient confirmations based on finality threshold |
| Have attestation but mint fails | Gas issues, incorrect parameters, or nonce already used | See [Retry a Failed Mint](/cctp/howtos/retry-failed-mint) |
# Withdraw USDC from HyperCore to EVM chains
Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-evm
This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance
to an external EVM blockchain (such as Arbitrum, Ethereum, or Base) using the
HyperCore API. Withdrawals from HyperCore to EVM chains default to the Fast
Transfer method, due to the fast finality of HyperEVM.
The withdrawal process:
1. Debits your HyperCore balance (`spot` or `perp`)
2. Routes through HyperEVM where USDC is burned via CCTP
3. CCTP attests to the burn and mints on the destination chain
4. If automatic forwarding is enabled, the recipient receives funds directly
Withdrawals include a HyperCore fee and (if using the Forwarding Service) a CCTP
forwarding fee. Ensure your withdrawal amount exceeds combined fees depending on
your transfer.
## Important considerations
Keep these things in mind when withdrawing USDC from HyperCore to EVM chains:
* **Data field:** If the data field is empty, the `CoreDepositWallet`
automatically sets a default hook that enables automatic message forwarding on
the destination blockchain, provided that the blockchain supports CCTP
forwarding. If the data field is not empty, its contents are passed to the
CCTP protocol as the value of the `hookData` field.
* **Destination caller:** The CCTP `destinationCaller` is always set to the zero
address. Passing your own hook data means that anyone can receive the message
on the destination blockchain.
* **Withdrawal fees:** In addition to the `maxFee` charged by the HyperCore
blockchain, an additional fixed forwarding fee may be charged by CCTP if
automatic forwarding is enabled. The forwarding fee amount depends on the
destination blockchain and can be viewed by querying the `CoreDepositWallet`
smart contract. Initially, the fee for forwarding to Arbitrum is 0.2 USDC. If
the withdrawal includes custom hook data, the forwarding fee is not set and
users have to receive the message on the destination blockchain themselves.
* **Minimum withdrawal amount:** If the withdrawal amount is less than the
required forwarding fee, the transaction on HyperEVM reverts. Make sure the
withdrawal amount is larger than the fees.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM wallet with the private key available
* Funded your HyperCore account with USDC in either `spot` or `perp` balance
* Created a new Node project and installed dependencies:
```bash theme={null}
npm install ethers
npm install -D tsx typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
DESTINATION_RECIPIENT=0x... # Recipient address on destination chain
```
## Steps
Use the following steps to withdraw USDC from HyperCore to an EVM blockchain.
### Step 1. Construct the `sendToEvmWithData` action
Create a `sendToEvmWithData` action object with the following parameters:
* `type`: `sendToEvmWithData`
* `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet)
* `signatureChainId`: The destination chain's EVM chain ID in hexadecimal format
(e.g., `"0xa4b1"` for Arbitrum, `"0x1"` for Ethereum). Must match the
destination chain.
* `token`: `USDC`
* `amount`: The amount of USDC as a string (e.g., `"10"` for 10 USDC, `"1.5"`
for 1.5 USDC)
* `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for `perp`
balance
* `destinationRecipient`: The recipient address on the destination blockchain
* `addressEncoding`: `hex` for EVM chains or `base58` for Solana
* `destinationChainId`: The CCTP destination domain ID (for example, `3` for
Arbitrum, `0` for Ethereum, `6` for Base)
* `gasLimit`: Gas limit for the transaction on the destination chain
* `data`: CCTP hook data (use `"0x"` for automatic forwarding)
* `nonce`: Current timestamp in milliseconds
```ts TypeScript theme={null}
// Example action payload for sendToEvmWithData
const action = {
type: "sendToEvmWithData",
hyperliquidChain: "Mainnet",
signatureChainId: "0xa4b1", // Arbitrum chain ID used for signing
token: "USDC",
amount: "10", // 10 USDC
sourceDex: "spot", // or "" for perp
destinationRecipient: "0x1234567890123456789012345678901234567890",
addressEncoding: "hex",
destinationChainId: 3, // Arbitrum CCTP domain
gasLimit: 200000,
data: "0x", // "0x" enables automatic forwarding on the destination
nonce: Date.now(),
};
```
### Step 2. Sign the action using EIP-712
Sign the action using the EIP-712 typed data signing standard. The signature
proves that you authorize this withdrawal.
The signing domain should include:
* `name`: `"HyperliquidSignTransaction"`
* `version`: `"1"`
* `chainId`: The chain ID from `signatureChainId` (as a number)
* `verifyingContract`: `"0x0000000000000000000000000000000000000000"`
```ts TypeScript theme={null}
import { Wallet, Signature } from "ethers";
// Sign the action using EIP-712
const wallet = new Wallet(privateKey);
const chainId = parseInt(signatureChainId, 16);
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
const types = {
"HyperliquidTransaction:SendToEvmWithData": [
{ name: "hyperliquidChain", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "sourceDex", type: "string" },
{ name: "destinationRecipient", type: "string" },
{ name: "addressEncoding", type: "string" },
{ name: "destinationChainId", type: "uint32" },
{ name: "gasLimit", type: "uint64" },
{ name: "data", type: "bytes" },
{ name: "nonce", type: "uint64" },
],
};
const message = {
hyperliquidChain: "Mainnet",
token: "USDC",
amount: "10",
sourceDex: "spot",
destinationRecipient: "0x...",
addressEncoding: "hex",
destinationChainId: 3,
gasLimit: BigInt(200000),
data: "0x",
nonce: BigInt(Date.now()),
};
const sigHex = await wallet.signTypedData(domain, types, message);
const sig = Signature.from(sigHex);
const signature = { r: sig.r, s: sig.s, v: sig.v };
```
### Step 3. Submit the signed action to the exchange API
Call the
[exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint)
endpoint with the action, nonce, and signature.
```ts TypeScript theme={null}
const response = await fetch("https://api.hyperliquid.xyz/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action,
nonce: timestamp,
signature,
}),
});
const result = await response.json();
if (response.status === 200 && result.status === "ok") {
console.log("Withdrawal initiated successfully:", result);
} else {
throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
}
```
## Full example code
The following is a complete example of how to withdraw USDC from HyperCore to an
external EVM blockchain. By default, it withdraws 10 USDC from your perp balance
to Arbitrum testnet with automatic forwarding enabled. For other destination
chains, update `destinationChainId` (CCTP domain ID) and `signatureChainId`
(destination chain's EVM chain ID in hex) accordingly.
```ts TypeScript expandable theme={null}
/**
* Script: Withdraw USDC from HyperCore to EVM chain
* - Signs EIP-712 sendToEvmWithData action
* - Submits to Hyperliquid /exchange API
*/
import { Wallet, Signature } from "ethers";
// -------- Configuration --------
const config = {
privateKey: process.env.PRIVATE_KEY as string,
// Transfer parameters
amount: process.env.AMOUNT || "10", // 10 USDC
sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot
// Destination parameters
destinationRecipient: process.env.DESTINATION_RECIPIENT as string,
destinationChainId: Number(process.env.DESTINATION_CHAIN_ID || 3), // 3 = Arbitrum
addressEncoding: process.env.ADDRESS_ENCODING || "hex",
gasLimit: Number(process.env.GAS_LIMIT || 200000),
data: process.env.DATA || "0x", // "0x" enables automatic forwarding
// Hyperliquid environment
isMainnet:
String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true",
signatureChainId: "0xa4b1", // Destination chain's EVM chain ID (hex) for EIP-712 signing
};
// -------- Main Function --------
async function main() {
// Validate required parameters
if (!config.privateKey) {
throw new Error("Set PRIVATE_KEY");
}
if (!config.destinationRecipient) {
throw new Error("Set DESTINATION_RECIPIENT");
}
const apiUrl = config.isMainnet
? "https://api.hyperliquid.xyz"
: "https://api.hyperliquid-testnet.xyz";
const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet";
const chainId = parseInt(config.signatureChainId, 16);
const timestamp = Date.now();
console.log("Withdrawing from HyperCore:", hyperliquidChain);
console.log("Source balance:", config.sourceDex || "perp");
console.log("Amount (USDC):", config.amount);
console.log("Destination recipient:", config.destinationRecipient);
console.log("Destination chain ID:", config.destinationChainId);
console.log("Gas limit:", config.gasLimit);
// EIP-712 Domain
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
// EIP-712 Types
const types = {
"HyperliquidTransaction:SendToEvmWithData": [
{ name: "hyperliquidChain", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "sourceDex", type: "string" },
{ name: "destinationRecipient", type: "string" },
{ name: "addressEncoding", type: "string" },
{ name: "destinationChainId", type: "uint32" },
{ name: "gasLimit", type: "uint64" },
{ name: "data", type: "bytes" },
{ name: "nonce", type: "uint64" },
],
};
// Message to sign
const message = {
hyperliquidChain,
token: "USDC",
amount: config.amount,
sourceDex: config.sourceDex,
destinationRecipient: config.destinationRecipient,
addressEncoding: config.addressEncoding,
destinationChainId: config.destinationChainId,
gasLimit: BigInt(config.gasLimit),
data: config.data,
nonce: BigInt(timestamp),
};
// Sign the message using EIP-712
const wallet = new Wallet(config.privateKey);
const sigHex = await wallet.signTypedData(domain, types, message);
const sig = Signature.from(sigHex);
// Build action payload
const action = {
type: "sendToEvmWithData",
hyperliquidChain,
signatureChainId: config.signatureChainId,
token: "USDC",
amount: config.amount,
sourceDex: config.sourceDex,
destinationRecipient: config.destinationRecipient,
addressEncoding: config.addressEncoding,
destinationChainId: config.destinationChainId,
gasLimit: config.gasLimit,
data: config.data,
nonce: timestamp,
};
// Submit to Hyperliquid exchange API
const response = await fetch(`${apiUrl}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action,
nonce: timestamp,
signature: { r: sig.r, s: sig.s, v: sig.v },
}),
});
const result = await response.json();
console.log("\nStatus:", response.status);
console.log("Response:", JSON.stringify(result, null, 2));
if (response.status === 200 && result.status === "ok") {
console.log("\nWithdrawal initiated successfully");
} else {
throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
}
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
```
Run the script:
```bash theme={null}
npx tsx script.ts
```
# Withdraw USDC from HyperCore to HyperEVM
Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-hyperevm
This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance
to HyperEVM using the HyperCore API.
You can only withdraw USDC from HyperCore to the same address on HyperEVM. It's
not possible to specify a different recipient address.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM wallet with the private key available
* Funded your HyperCore account with USDC in either `spot` or `perp` balance
* Created a new Node project and installed dependencies:
```bash theme={null}
npm install ethers
npm install -D tsx typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
```
## Steps
Use the following steps to withdraw USDC from HyperCore to HyperEVM.
### Step 1. Construct the `sendAsset` action
Create a `sendAsset` action object with the following parameters:
* `type`: `sendAsset`
* `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet)
* `signatureChainId`: An EVM chain ID used for EIP-712 replay protection. Must
match between signing and the action payload, but can be any valid chain ID
(for example, `"0xa4b1"` for Arbitrum)
* `destination`: The USDC token system address
(`0x2000000000000000000000000000000000000000`)
* `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for perp balance
* `destinationDex`: `"spot"`
* `token`: `USDC`
* `amount`: The amount of USDC as a human-readable string (for example, `"10"`
for 10 USDC)
* `fromSubAccount`: Set to `""` for main account, or the subaccount address
* `nonce`: Current timestamp in milliseconds
```ts TypeScript theme={null}
const action = {
type: "sendAsset",
hyperliquidChain: "Testnet",
signatureChainId: "0xa4b1", // EVM chain ID for EIP-712 replay protection
destination: "0x2000000000000000000000000000000000000000",
sourceDex: "", // "" for perp, "spot" for spot
destinationDex: "spot",
token: "USDC",
amount: "10", // 10 USDC (human-readable)
fromSubAccount: "",
nonce: Date.now(),
};
```
### Step 2. Sign the action using EIP-712
Sign the action using the EIP-712 typed data signing standard. The signature
proves that you authorize this withdrawal.
The signing domain should include:
* `name`: `"HyperliquidSignTransaction"`
* `version`: `"1"`
* `chainId`: The chain ID from `signatureChainId` (as a number)
* `verifyingContract`: `"0x0000000000000000000000000000000000000000"`
```ts TypeScript theme={null}
import { Wallet, Signature } from "ethers";
async function signSendAssetAction(
action: any,
privateKey: string,
): Promise<{ r: string; s: string; v: number }> {
const wallet = new Wallet(privateKey);
// Convert chainId from hex to number
const chainId = parseInt(action.signatureChainId, 16);
// EIP-712 domain
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
// EIP-712 types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES)
const types = {
"HyperliquidTransaction:SendAsset": [
{ name: "hyperliquidChain", type: "string" },
{ name: "destination", type: "string" },
{ name: "sourceDex", type: "string" },
{ name: "destinationDex", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "fromSubAccount", type: "string" },
{ name: "nonce", type: "uint64" },
],
};
// Message to sign (only fields defined in EIP-712 types, not signatureChainId)
const value = {
hyperliquidChain: action.hyperliquidChain,
destination: action.destination,
sourceDex: action.sourceDex,
destinationDex: action.destinationDex,
token: action.token,
amount: action.amount,
fromSubAccount: action.fromSubAccount,
nonce: BigInt(action.nonce),
};
// Sign the typed data
const signature = await wallet.signTypedData(domain, types, value);
// Split signature into r, s, v components
const sig = Signature.from(signature);
return {
r: sig.r,
s: sig.s,
v: sig.v,
};
}
```
### Step 3. Submit the signed action to the exchange API
Call the
[exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#send-asset)
endpoint with the action, nonce, and signature.
```ts TypeScript theme={null}
async function submitSendAsset(
action: any,
signature: { r: string; s: string; v: number },
) {
// Use https://api.hyperliquid.xyz for mainnet
const response = await fetch("https://api.hyperliquid-testnet.xyz/exchange", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action: action,
nonce: action.nonce,
signature: signature,
}),
});
const data = await response.json();
if (data.status === "ok") {
console.log("Withdrawal successful:", data);
return data;
} else {
throw new Error(`Withdrawal failed: ${JSON.stringify(data)}`);
}
}
```
## Full example code
The following is a complete example of how to withdraw USDC from HyperCore to
HyperEVM. By default, it withdraws 10 USDC from your perp balance to HyperEVM
testnet.
```ts TypeScript expandable theme={null}
/**
* Script: Withdraw USDC from HyperCore to HyperEVM
* - Constructs a SendAsset action
* - Signs the action using EIP-712
* - Submits the signed action to the HyperCore API
*/
import { Wallet, Signature } from "ethers";
// -------- Configuration --------
const config = {
privateKey: process.env.PRIVATE_KEY as string,
// Transfer parameters
amount: process.env.AMOUNT || "10", // 10 USDC (human-readable)
sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot
// Hyperliquid environment
isMainnet:
String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true",
};
// System address for USDC token on HyperCore
const USDC_SYSTEM_ADDRESS = "0x2000000000000000000000000000000000000000";
// -------- Main Function --------
async function main() {
if (!config.privateKey) {
throw new Error("Set PRIVATE_KEY");
}
const apiUrl = config.isMainnet
? "https://api.hyperliquid.xyz"
: "https://api.hyperliquid-testnet.xyz";
const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet";
const signingChainId = "0xa4b1"; // EVM chain ID for EIP-712 signing (any valid chain ID works)
const chainId = parseInt(signingChainId, 16);
const timestamp = Date.now();
const wallet = new Wallet(config.privateKey);
console.log("Withdrawing from HyperCore to HyperEVM:", hyperliquidChain);
console.log("User Address:", wallet.address);
console.log("Source balance:", config.sourceDex || "perp");
console.log("Amount (USDC):", config.amount);
// EIP-712 Domain
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
// Build action for signing
const actionForSigning = {
hyperliquidChain,
signatureChainId: signingChainId,
destination: USDC_SYSTEM_ADDRESS,
sourceDex: config.sourceDex,
destinationDex: "spot",
token: "USDC",
amount: config.amount,
fromSubAccount: "",
nonce: timestamp,
};
// EIP-712 Types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES)
const types = {
"HyperliquidTransaction:SendAsset": [
{ name: "hyperliquidChain", type: "string" },
{ name: "destination", type: "string" },
{ name: "sourceDex", type: "string" },
{ name: "destinationDex", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "fromSubAccount", type: "string" },
{ name: "nonce", type: "uint64" },
],
};
// Message to sign (only fields defined in EIP-712 types, not signatureChainId)
const message = {
hyperliquidChain: actionForSigning.hyperliquidChain,
destination: actionForSigning.destination,
sourceDex: actionForSigning.sourceDex,
destinationDex: actionForSigning.destinationDex,
token: actionForSigning.token,
amount: actionForSigning.amount,
fromSubAccount: actionForSigning.fromSubAccount,
nonce: BigInt(actionForSigning.nonce),
};
// Sign the message using EIP-712
const sigHex = await wallet.signTypedData(domain, types, message);
const sig = Signature.from(sigHex);
// Build action payload for API (includes type and signatureChainId)
const action: any = {
type: "sendAsset",
...actionForSigning,
};
// Submit to Hyperliquid exchange API
const response = await fetch(`${apiUrl}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action,
nonce: timestamp,
signature: { r: sig.r, s: sig.s, v: sig.v },
}),
});
const result = await response.json();
console.log("\nStatus:", response.status);
console.log("Response:", JSON.stringify(result, null, 2));
if (response.status === 200 && result.status === "ok") {
console.log("\nWithdrawal initiated successfully");
} else {
throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
}
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
```
Run the script:
```bash theme={null}
npx tsx script.ts
```
# Migrate from CCTP V1 (Legacy) to V2
Source: https://developers.circle.com/cctp/migration-from-v1-to-v2
Complete migration guide for developers upgrading CCTP integrations
This guide provides a summary of the breaking changes when migrating from
Cross-Chain Transfer Protocol (CCTP) V1 to V2. CCTP V2 introduces enhancements
including Fast Transfer, Hooks features, and improved API endpoints, but
requires updating your integration due to breaking changes.
**Important**: CCTP V2 isn't backward compatible with V1. It uses separate
contracts, APIs, and transfer speeds. It also introduces new blockchain support,
while deprecating some chains. Plan for a complete integration update rather
than incremental changes.
Failure to migrate will eventually result in loss of crosschain capabilities for
your integration.
Arc App Kit's [Bridge](https://docs.arc.io/app-kit/bridge) capability can help
simplify your migration to CCTP V2. See the
[Migrating with App Kit](#migrating-with-app-kit) section for more information.
## V1 deprecation
Circle is deprecating CCTP V1 to focus on the newer version, which is upgradable
and provides a faster, more secure, and more robust crosschain experience across
a wider network of blockchains.
### Naming changes
CCTP V2 is now referred to as CCTP (except in this document). The V1 version of
CCTP is now CCTP V1 (Legacy).
### Deprecation timeline
CCTP V1 will be phased out over the course of 10 months beginning in July 2026.
CCTP V2 contracts are available on all CCTP V1 chains except for Aptos, Noble,
and Sui. Aptos and Sui will be supported by V2 before the phase out begins.
Circle is working with Noble and Cosmos ecosystem teams on an intermediate
solution to route USDC flows to and from Noble.
### Access to funds
You will not lose access to funds during the V1 phase out. All pending
redemptions will remain available as CCTP V1 (legacy) begins its phase out.
Circle will maintain minter allowances greater than the total of pending
attestations, ensuring every redemption can be processed before V1 contracts are
fully paused.
The deprecation process is designed to wind down activity gradually, message
limits will tighten over time until no new burns can be initiated, bringing
transfer volume to zero before contracts are fully paused.
### Additional resources
In addition to this guide and [Arc App Kit](https://docs.arc.io/app-kit), you
can contact the Circle team on the
[BuildOnCircle Discord](https://discord.com/invite/buildoncircle) for questions
and migration support.
## Summary of breaking changes
The latest version of CCTP introduces architectural changes that make it
incompatible with V1 integrations. You must update your implementation to use
the new contracts, APIs, and transfer speeds. Additionally, the overall flow of
the protocol has been streamlined, which means you need to update your
integration to use the new functions.
* Contracts are deployed at
[different addresses](/cctp/references/contract-addresses) than V1 contracts.
You should update your integration to point to the new contract addresses.
* [Contract interfaces](/cctp/references/contract-interfaces) have changed.
Importantly, the
[`depositForBurn` function](/cctp/references/contract-interfaces#depositforburn)
now takes additional parameters. You should update your integration to use the
new ABIs and contract calls.
* CCTP now allows you to specify a transfer speed. The `finalityThreshold`
parameter specifies whether the transfer should be a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times).
* You no longer need to extract the message from the onchain transaction to
fetch an attestation. Instead, you can call the new
`/v2/messages/{sourceDomainId}` endpoint with the transaction hash to get the
message and attestation in a single call.
* API endpoints have changed. The new `/v2/` endpoints have different functions
than the old `/v1/` endpoints. You should update your integration to use the
new endpoints. Review the
[CCTP API reference](/api-reference/cctp/all/get-public-keys-v2) for details
on the changes to the CCTP offchain API.
* [Fees](/cctp/concepts/fees) have been introduced. Fast Transfer has a variable
fee based on the source chain. You should update your integration to account
for the new fees.
## Migrating with App Kit
[Arc App Kit](https://docs.arc.io/app-kit) provides a simplified migration path
by abstracting routine setup steps and standardizing bridging flows. This
enables you to integrate bridging operations with minimal code.
### Benefits of using App Kit to bridge
* **No contract management**: App Kit handles contract addresses, ABIs, and
function calls for you.
* **No attestation polling**: Automatically retrieves attestations without
manual API calls.
* **Built-in CCTP features**: Access Fast Transfer and other capabilities
through simple configuration.
* **Type-safe interface**: Compatible with `viem` and `ethers` for safer
development.
* **Fee collection**: Optionally collect fees from transfers to monetize your
application.
### Example migration
Replace manual contract calls and API polling with a single method:
```typescript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
// Initialize App Kit
const kit = new AppKit();
// Create adapter for your wallet
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
});
// Transfer USDC with Fast Transfer
const result = await kit.bridge({
from: { adapter, chain: "Ethereum" },
to: { adapter, chain: "Base" },
amount: "100",
config: {
transferSpeed: "FAST", // Use Fast Transfer
maxFee: "5000000", // Max 5 USDC fee (optional)
},
});
// Result includes transaction details and explorer URLs
console.log("Transfer complete:", result.steps);
```
For more information, see Arc App Kit's
[Bridge](https://docs.arc.io/app-kit/bridge) capability.
## Changes to smart contracts
CCTP uses new smart contracts with different names, addresses, and interfaces.
You must update your integration to use the new contracts and their new function
signatures.
### Contract name and address changes
All legacy contracts have V2 equivalents deployed at new addresses:
| Legacy contract | V2 contract | Documentation |
| -------------------- | ---------------------- | --------------------------------------------------------------- |
| `TokenMessenger` | `TokenMessengerV2` | [V2 Interface](/cctp/evm-smart-contracts#tokenmessengerv2) |
| `MessageTransmitter` | `MessageTransmitterV2` | [V2 Interface](/cctp/evm-smart-contracts#messagetransmitterv2) |
| `TokenMinter` | `TokenMinterV2` | [V2 Addresses](/cctp/evm-smart-contracts#tokenminterv2-mainnet) |
| `Message` | `MessageV2` | [V2 Addresses](/cctp/evm-smart-contracts#messagev2-mainnet) |
**Important**: V2 contracts are deployed at different addresses than V1
contracts. See the
[CCTP Contract Addresses](/cctp/evm-smart-contracts#mainnet-contract-addresses)
for the complete list of mainnet and testnet addresses.
### TokenMessengerV2 changes
**Modified functions:**
* `depositForBurn()` now requires three additional parameters:
* `destinationCaller` (bytes32) - Address that can call `receiveMessage` on
destination
* `maxFee` (uint256) - Maximum fee for Fast Transfer in units of burn token
* `minFinalityThreshold` (uint32) - Minimum finality level (1000 for Fast,
2000 for Standard)
**New functions:**
* `depositForBurnWithHook()` - Enables custom logic execution on destination
chain via hook data
* `getMinFeeAmount()` - Calculates minimum fee for Standard Transfer (on
supported chains only)
**Removed functions:**
* `depositForBurnWithCaller()` - Use `destinationCaller` parameter in
`depositForBurn()` instead
* `replaceDepositForBurn()` - No V2 equivalent available
### Contract source code
Full contract source code is available on GitHub:
* [CCTP EVM Contracts](https://github.com/circlefin/evm-cctp-contracts) - Main
repository
* [Contract ABIs](https://github.com/circlefin/evm-cctp-contracts/tree/master/docs/abis/cctp/v2) -
Interface definitions
## API migration guide
CCTP streamlines the API workflow by combining message retrieval and attestation
into single calls, while introducing new endpoints for features like Fast
Transfer monitoring and re-attestation.
### Workflow changes
The API eliminates the need to extract the message emitted by the onchain
transaction:
**Legacy workflow:**
1. Get the transaction receipt from the onchain transaction
2. Find the MessageSent event in the transaction receipt
3. Hash the message bytes emitted by the MessageSent event
4. Call `/v1/attestations/{messageHash}` to get an attestation
**V2 workflow:**
1. Call `/v2/messages/{sourceDomainId}` with transaction hash or nonce to get
message, attestation, and decoded data
#### Legacy workflow example
```javascript theme={null}
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
// V1 requires multiple steps to extract message and get attestation
const burnTxHash = "0x1234..."; // Transaction hash from depositForBurn
// Step 1: Get the transaction receipt from the onchain transaction
const client = createPublicClient({
chain: sepolia,
transport: http(),
});
const transactionReceipt = await client.getTransactionReceipt({
hash: burnTxHash,
});
// Step 2: Find the MessageSent event in the transaction receipt
const eventTopic = keccak256(toBytes("MessageSent(bytes)"));
const log = transactionReceipt.logs.find((l) => l.topics[0] === eventTopic);
const messageBytes = decodeAbiParameters([{ type: "bytes" }], log.data)[0];
// Step 3: Hash the message bytes emitted by the MessageSent event
const messageHash = keccak256(messageBytes);
// Step 4: Call attestation API with the message hash
let attestationResponse = { status: "pending" };
while (attestationResponse.status !== "complete") {
const response = await fetch(
`https://iris-api-sandbox.circle.com/attestations/${messageHash}`,
);
attestationResponse = await response.json();
await new Promise((r) => setTimeout(r, 2000));
}
const attestation = attestationResponse.attestation;
// Now you can use messageBytes and attestation to call receiveMessage
```
#### V2 workflow example
```javascript theme={null}
// V2 gets message and attestation in a single call
const sourceDomainId = 0; // Ethereum mainnet
const transactionHash = "0x1234...";
// Single step: Get message, attestation, and decoded data
const response = await fetch(
`https://iris-api.circle.com/v2/messages/${sourceDomainId}?transactionHash=${transactionHash}`,
);
const data = await response.json();
// All data available in single response
const message = data.messages[0].message;
const attestation = data.messages[0].attestation;
const decodedMessage = data.messages[0].decodedMessage;
// Now you can use message and attestation to call receiveMessage
// You can also access decoded fields without manual parsing
console.log(`Amount: ${decodedMessage.decodedMessageBody.amount}`);
console.log(`Recipient: ${decodedMessage.decodedMessageBody.mintRecipient}`);
```
### Endpoint migration mapping
| Legacy endpoint | V2 replacement | Migration notes |
| ----------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ |
| `GET /v1/attestations/{messageHash}` | `GET /v2/messages/{sourceDomainId}?transactionHash={hash}` | Combined into messages endpoint with enhanced response |
| `GET /v1/messages/{sourceDomainId}/{transactionHash}` | `GET /v2/messages/{sourceDomainId}?transactionHash={hash}` | Enhanced with decoded data and attestation |
| `GET /v1/publicKeys` | `GET /v2/publicKeys` | Multi-version support, backward compatible |
### New V2-only endpoints
V2 introduces additional endpoints for advanced features:
| Endpoint | Purpose | Use case |
| -------------------------------------------------------- | --------------------------------- | ------------------------------------------------------ |
| `POST /v2/reattest/{nonce}` | Re-attest messages for edge cases | Handle expired Fast Transfer burns or finality changes |
| `GET /v2/fastBurn/USDC/allowance` | Monitor Fast Transfer allowance | Check remaining Fast Transfer capacity in real-time |
| `GET /v2/burn/USDC/fees/{sourceDomainId}/{destDomainId}` | Get current transfer fees | Calculate fees before initiating transfers |
### Message data changes
V2 message responses now include the decoded message data and attestation:
#### V1 messages response
```json theme={null}
{
"messages": [
{
"attestation": "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d4387756990c9c4b9280338325252866861f9495dce3128cd524d525c44e8e7b731dedd3098a618dcc19c45be1e1c",
"message": "0x00000000000000050000000300000000000194c2...",
"eventNonce": "9682"
}
]
}
```
#### V2 messages response
```json theme={null}
{
"messages": [
{
"message": "0x00000000000000050000000300000000000194c2...",
"eventNonce": "9682",
"attestation": "0x6edd90f4a0ad0212fd9fbbd5058a25aa8ee10ce77e4fc143567bbe73fb6e164f384a3e14d350c8a4fc50b781177297e03c16b304e8d7656391df0f59a75a271f1b",
"decodedMessage": {
"sourceDomain": "7",
"destinationDomain": "5",
"nonce": "569",
"sender": "0xca9142d0b9804ef5e239d3bc1c7aa0d1c74e7350",
"recipient": "0xb7317b4EFEa194a22bEB42506065D3772C2E95EF",
"destinationCaller": "0xf2Edb1Ad445C6abb1260049AcDDCA9E84D7D8aaA",
"messageBody": "0x00000000000000050000000300000000000194c2...",
"decodedMessageBody": {
"burnToken": "0x4Bc078D75390C0f5CCc3e7f59Ae2159557C5eb85",
"mintRecipient": "0xb7317b4EFEa194a22bEB42506065D3772C2E95EF",
"amount": "5000",
"messageSender": "0xca9142d0b9804ef5e239d3bc1c7aa0d1c74e7350"
}
},
"cctpVersion": 2,
"status": "complete"
}
]
}
```
On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. For inbound transfers, use
[`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
so funds reach the correct recipient. See
[CCTP on Stellar](/cctp/references/stellar).
# Transfer USDC from Ethereum to Arc
Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-ethereum-to-arc
Build a script to transfer USDC between EVM blockchains using CCTP
This guide demonstrates how to transfer USDC from Ethereum Sepolia to Arc
testnet using CCTP. You use the [viem](https://viem.sh/) framework to interact
with [CCTP contracts](/cctp/references/contract-addresses) and the
[CCTP API](/api-reference/cctp/all/get-messages-v2) to retrieve attestations.
**Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to
simplify crosschain transfers with CCTP.**
This quickstart shows how to transfer USDC from to
using a manual CCTP integration. The example is for learning
or for developers who need a manual integration.
To streamline this, use Bridge Kit to transfer USDC in just a few lines of code.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM testnet wallet with the private key available
* Added Arc testnet network to your wallet
([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
* Funded your wallet with the following testnet tokens:
* Sepolia ETH (native token) from a
[public faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
* Sepolia USDC from the [Circle Faucet](https://faucet.circle.com)
* Arc testnet USDC from the [Circle Faucet](https://faucet.circle.com) if you
choose the direct mint path below, because the destination wallet must pay
gas to call `receiveMessage`
## Step 1. Set up the project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir cctp-evm-transfer
cd cctp-evm-transfer
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install viem
# Install dev dependencies
npm install --save-dev @types/node tsx typescript
```
### 1.2. Configure TypeScript (optional)
This step is optional. It helps prevent missing types in your IDE or editor.
Create a `tsconfig.json` file:
```shell theme={null}
npx tsc --init
```
Then, update the `tsconfig.json` file:
```shell theme={null}
cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
```
### 1.3. Set environment variables
Open `.env` in your editor and add:
```text theme={null}
PRIVATE_KEY=YOUR_ETHEREUM_SEPOLIA_PRIVATE_KEY
```
* `PRIVATE_KEY` is the private key for the Ethereum Sepolia EOA that signs the
source-chain approval and burn transactions. The direct-mint path also uses
the same key to submit the destination mint on Arc.
Open `.env` in your editor rather than writing values with shell commands, and
add `.env` to your `.gitignore`. This prevents credentials from leaking into
your shell history or version control.
The `npm run start` command loads variables from `.env` using Node.js native
env-file support.
This example uses one or more private keys for local testing. In production,
use a secure key management solution and never expose or share private keys.
## Step 2: Configure the script
This section covers the necessary setup for the transfer script, including
defining keys and addresses, and configuring the wallet client for interacting
with the source and destination chains.
### 2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and maximum fee.
Update the `DESTINATION_ADDRESS` with your wallet address.
For simplicity, this quickstart uses the same EOA as the Ethereum Sepolia source
signer and the Arc recipient. In production, these can be different addresses.
```ts TypeScript theme={null}
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
// Contract Addresses
const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238";
const ETHEREUM_SEPOLIA_TOKEN_MESSENGER =
"0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa";
const ARC_TESTNET_MESSAGE_TRANSMITTER =
"0xe737e5cebeeba77efe34d4aa090756590b1ce275";
// Transfer Parameters
const DESTINATION_ADDRESS = account.address; // Address to receive minted tokens on destination chain
const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
const maxFee = 500n; // 0.0005 USDC (500 subunits)
// Bytes32 Formatted Parameters
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
2,
)}`; // Destination address in bytes32 format
const DESTINATION_CALLER_BYTES32 =
"0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage()
// Chain-specific Parameters
const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc testnet
```
### 2.2. Set up wallet clients
The wallet client configures the appropriate network settings using `viem`. The
direct-mint path below uses clients for both Ethereum Sepolia and Arc testnet.
The [Forwarding Service](/cctp/concepts/forwarding-service) path only needs the
source-chain client on Ethereum Sepolia.
```ts TypeScript theme={null}
// Set up the wallet clients
const sepoliaClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
const arcClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account,
});
```
## Step 3: Implement the transfer logic
The following sections outline the core transfer logic.
The path diverges at the source-chain burn transaction:
* **Direct mint** uses `depositForBurn`, then retrieves an attestation and calls
`receiveMessage` on Arc.
* **Forwarding Service** uses `depositForBurnWithHook`, then lets Circle handle
the destination-side mint on Arc.
### 3.1. Get forwarding fees and calculate the burn amount
Before you burn USDC with the Forwarding Service, query the CCTP fee endpoint
with `forward=true`. The forwarding fee is dynamic, so fetch it immediately
before the transfer. The returned `maxFee` must cover both the CCTP protocol fee
and the forwarding fee.
```ts TypeScript theme={null}
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`;
async function getForwardingFees() {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${ETHEREUM_SEPOLIA_DOMAIN}/${ARC_TESTNET_DOMAIN}?forward=true`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (!response.ok) {
throw new Error(`Failed to fetch fees: ${await response.text()}`);
}
return response.json();
}
async function calculateForwardingAmounts() {
const fees = await getForwardingFees();
const feeData = fees.find(
(fee: { finalityThreshold: number }) => fee.finalityThreshold === 1000,
);
if (!feeData) {
throw new Error("Fast-transfer forwarding fees not available");
}
const forwardFee = BigInt(feeData.forwardFee.med);
const protocolFee =
(AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = AMOUNT + maxFee;
return { maxFee, totalAmount };
}
```
### 3.2. Approve the total burn amount
Approve the total amount you will burn on the source chain. For the forwarding
path, that is the transfer amount plus the forwarding and protocol fees.
```ts TypeScript theme={null}
async function approveUSDC(amount: bigint) {
console.log("Approving USDC transfer...");
const approveTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, amount],
}),
});
console.log(`USDC Approval Tx: ${approveTx}`);
}
```
### 3.3. Burn USDC with the Forwarding Service hook
Use `depositForBurnWithHook` on the source chain. The forwarding hook data tells
Circle to handle the destination-side `receiveMessage` call on Arc.
```ts TypeScript theme={null}
async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) {
console.log("Burning USDC on Ethereum Sepolia with Forwarding Service...");
const burnTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
totalAmount,
ARC_TESTNET_DOMAIN,
DESTINATION_ADDRESS_BYTES32,
ETHEREUM_SEPOLIA_USDC,
DESTINATION_CALLER_BYTES32,
maxFee,
1000,
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
```
### 3.4. Verify the forwarded mint
After the burn is confirmed, poll the Iris API until it returns a
`forwardTxHash`. That hash is the Arc destination mint transaction submitted by
Circle. In the forwarding path, `forwardTxHash` is the completion signal for the
destination-side mint. You do not need to retrieve an attestation and call
`receiveMessage` yourself.
```ts TypeScript theme={null}
async function waitForForwardedMint(transactionHash: string) {
console.log("Waiting for Forwarding Service to mint on Arc...");
while (true) {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`,
{ method: "GET" },
);
if (!response.ok) {
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = await response.json();
const forwardTxHash = data?.messages?.[0]?.forwardTxHash;
if (forwardTxHash) {
console.log(`Forwarded Mint Tx: ${forwardTxHash}`);
return forwardTxHash;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
```
### 3.1. Approve USDC
Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on
Ethereum Sepolia to withdraw USDC from your wallet. This allows the contract to
burn USDC when you initiate the transfer.
```ts TypeScript theme={null}
async function approveUSDC() {
console.log("Approving USDC transfer...");
const approveTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000n], // 10 USDC allowance
}),
});
console.log(`USDC Approval Tx: ${approveTx}`);
}
```
### 3.2. Burn USDC
Call the `depositForBurn` function from the
[`TokenMessengerV2` contract](/cctp/references/contract-interfaces#depositforburn)
deployed on Ethereum Sepolia to burn USDC on that source chain. You specify the
following parameters:
* **Burn amount**: The amount of USDC to burn
* **Destination domain**: The target blockchain for minting USDC (see
[supported chains and domains](/cctp/concepts/supported-chains-and-domains))
* **Mint recipient**: The wallet address that will receive the minted USDC
* **Burn token**: The contract address of the USDC token being burned on the
source chain
* **Destination caller**: The address on the target chain to call
`receiveMessage`
* **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
* **Finality threshold**: Determines whether it's a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
(1000 or less) or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
(2000 or more)
```ts TypeScript theme={null}
async function burnUSDC() {
console.log("Burning USDC on Ethereum Sepolia...");
const burnTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurn",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
],
outputs: [],
},
],
functionName: "depositForBurn",
args: [
AMOUNT,
ARC_TESTNET_DOMAIN,
DESTINATION_ADDRESS_BYTES32,
ETHEREUM_SEPOLIA_USDC,
DESTINATION_CALLER_BYTES32,
maxFee,
1000, // minFinalityThreshold (1000 or less for Fast Transfer)
],
}),
});
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
```
### 3.3. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling
Circle's attestation API.
* Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2)
API endpoint to retrieve the attestation.
* Pass the `srcDomain` argument from the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for your source chain.
* Pass `transactionHash` from the value returned by `sendTransaction` within the
`burnUSDC` function above.
```ts TypeScript theme={null}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
```
### 3.4. Mint USDC
Call the
[`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage)
from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses)
deployed on the Arc testnet to mint USDC on that destination chain.
* Pass the signed attestation and the message data as parameters.
* The function processes the attestation and mints USDC to the specified Arc
testnet wallet address.
```ts TypeScript theme={null}
async function mintUSDC(attestation: AttestationMessage) {
console.log("Minting USDC on Arc testnet...");
const mintTx = await arcClient.sendTransaction({
to: ARC_TESTNET_MESSAGE_TRANSMITTER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
console.log(`Mint Tx: ${mintTx}`);
}
```
## Step 4: Complete script
Create a `index.ts` file in your project directory and populate it with the
complete code below for the path you want to test.
```ts index.ts expandable theme={null}
import {
createPublicClient,
createWalletClient,
http,
encodeFunctionData,
pad,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
type FeeQuote = {
finalityThreshold: number;
minimumFee: number;
forwardFee: { med: number };
};
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238";
const ETHEREUM_SEPOLIA_TOKEN_MESSENGER =
"0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa";
const DESTINATION_ADDRESS = account.address;
const AMOUNT = 1_000_000n;
const ETHEREUM_SEPOLIA_DOMAIN = 0;
const ARC_TESTNET_DOMAIN = 26;
const DESTINATION_ADDRESS_BYTES32 = pad(DESTINATION_ADDRESS, { size: 32 });
const DESTINATION_CALLER_BYTES32 = pad("0x", { size: 32 });
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`;
const sepoliaClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
const sepoliaPublicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
async function approveUSDC(amount: bigint) {
console.log("Approving USDC transfer...");
const approveTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, amount],
}),
});
console.log(`USDC Approval Tx: ${approveTx}`);
await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx });
}
async function getForwardingFeeQuote() {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${ETHEREUM_SEPOLIA_DOMAIN}/${ARC_TESTNET_DOMAIN}?forward=true`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (!response.ok) {
throw new Error(`Failed to fetch fees: ${await response.text()}`);
}
const fees = (await response.json()) as FeeQuote[];
const feeData = fees.find((fee) => fee.finalityThreshold === 1000);
if (!feeData) {
throw new Error("Fast-transfer forwarding fees not available");
}
return feeData;
}
async function calculateForwardingAmounts() {
const feeData = await getForwardingFeeQuote();
const forwardFee = BigInt(feeData.forwardFee.med);
const protocolFee =
(AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = AMOUNT + maxFee;
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
return { maxFee, totalAmount };
}
async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) {
console.log("Burning USDC on Ethereum Sepolia with Forwarding Service...");
const burnTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
totalAmount,
ARC_TESTNET_DOMAIN,
DESTINATION_ADDRESS_BYTES32,
ETHEREUM_SEPOLIA_USDC,
DESTINATION_CALLER_BYTES32,
maxFee,
1000,
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
async function waitForForwardedMint(transactionHash: string) {
console.log("Waiting for Forwarding Service to mint on Arc...");
while (true) {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`,
{ method: "GET" },
);
if (!response.ok) {
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = await response.json();
const forwardTxHash = data?.messages?.[0]?.forwardTxHash;
if (forwardTxHash) {
console.log(`Forwarded Mint Tx: ${forwardTxHash}`);
return forwardTxHash;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
async function main() {
console.log("Wallet address:", account.address);
// [1] Quote forwarding fees and derive the total source-chain burn amount.
const { maxFee, totalAmount } = await calculateForwardingAmounts();
// [2] Approve the total burn amount, including forwarding and protocol fees.
await approveUSDC(totalAmount);
// [3] Burn on the source chain with the forwarding hook enabled.
const burnTx = await burnUSDCWithForwarding(totalAmount, maxFee);
// [4] Poll until Iris returns the destination mint transaction hash.
await waitForForwardedMint(burnTx);
console.log("USDC transfer completed with Forwarding Service.");
}
main().catch(console.error);
```
```ts index.ts expandable theme={null}
import {
createPublicClient,
createWalletClient,
http,
encodeFunctionData,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet, sepolia } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// ============ Configuration Constants ============
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
// Contract Addresses
const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238";
const ETHEREUM_SEPOLIA_TOKEN_MESSENGER =
"0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa";
const ARC_TESTNET_MESSAGE_TRANSMITTER =
"0xe737e5cebeeba77efe34d4aa090756590b1ce275";
// Transfer Parameters
const DESTINATION_ADDRESS = account.address; // Address to receive minted tokens on destination chain
const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
const maxFee = 500n; // 0.0005 USDC (500 subunits)
// Bytes32 Formatted Parameters
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
2,
)}`; // Destination address in bytes32 format
const DESTINATION_CALLER_BYTES32 =
"0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage()
// Chain-specific Parameters
const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc testnet
// Set up wallet clients
const sepoliaClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
const sepoliaPublicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const arcClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account,
});
// ============ CCTP Flow Functions ============
async function approveUSDC() {
console.log("Approving USDC transfer...");
const approveTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000n], // 10 USDC allowance
}),
});
console.log(`USDC Approval Tx: ${approveTx}`);
await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx });
}
async function burnUSDC() {
console.log("Burning USDC on Ethereum Sepolia...");
const burnTx = await sepoliaClient.sendTransaction({
to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurn",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
],
outputs: [],
},
],
functionName: "depositForBurn",
args: [
AMOUNT,
ARC_TESTNET_DOMAIN,
DESTINATION_ADDRESS_BYTES32 as `0x${string}`,
ETHEREUM_SEPOLIA_USDC,
DESTINATION_CALLER_BYTES32,
maxFee,
1000, // minFinalityThreshold (1000 or less for Fast Transfer)
],
}),
});
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function mintUSDC(attestation: AttestationMessage) {
console.log("Minting USDC on Arc testnet...");
const mintTx = await arcClient.sendTransaction({
to: ARC_TESTNET_MESSAGE_TRANSMITTER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
console.log(`Mint Tx: ${mintTx}`);
}
// ============ Main Execution ============
async function main() {
await approveUSDC();
const burnTx = await burnUSDC();
const attestation = await retrieveAttestation(burnTx);
await mintUSDC(attestation);
console.log("USDC transfer completed.");
}
main().catch(console.error);
```
## Step 5: Test the script
Run the following command to execute the script:
```shell Shell theme={null}
npm run start
```
Once the script runs and the transfer is finalized, a confirmation receipt is
logged in the console.
**Rate limit:** The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.
# Transfer USDC from Solana to Arc
Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-solana-to-arc
Transfer USDC from Solana to an EVM blockchain using CCTP
This guide demonstrates how to transfer USDC from Solana Devnet to Arc Testnet
using CCTP. You use the [Solana Kit](https://github.com/anza-xyz/kit) library to
interact with [Solana CCTP programs](/cctp/references/solana-programs), and viem
to mint USDC on Arc Testnet.
**Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to
simplify crosschain transfers with CCTP.**
This quickstart shows how to transfer USDC from to
using a manual CCTP integration. The example is for learning
or for developers who need a manual integration.
To streamline this, use Bridge Kit to transfer USDC in just a few lines of code.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared a Solana wallet and have the private key array available
* Funded your Solana wallet with the following testnet tokens:
* Solana Devnet SOL (native token) from a
[public faucet](https://faucet.solana.com/)
* Solana Devnet USDC from the [Circle Faucet](https://faucet.circle.com)
* Prepared an EVM testnet wallet with the private key available
* Added Arc testnet network to your wallet
([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
* Funded your EVM wallet with Arc Testnet USDC from the
[Circle Faucet](https://faucet.circle.com) if you choose the direct mint path
below, because the destination wallet must pay gas to call `receiveMessage`
## Step 1. Set up the project
### 1.1. Create the project and install dependencies
```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir cctp-solana-transfer
cd cctp-solana-transfer
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install @solana/kit @solana-program/system @solana-program/token viem
# Install dev dependencies
npm install --save-dev @types/node tsx typescript
```
### 1.2. Configure TypeScript (optional)
This step is optional. It helps prevent missing types in your IDE or editor.
Create a `tsconfig.json` file:
```shell theme={null}
npx tsc --init
```
Then, update the `tsconfig.json` file:
```shell theme={null}
cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
```
### 1.3. Set environment variables
Open `.env` in your editor and add:
```text theme={null}
SOLANA_PRIVATE_KEY=YOUR_SOLANA_PRIVATE_KEY_ARRAY
EVM_PRIVATE_KEY=YOUR_ARC_PRIVATE_KEY
```
* `SOLANA_PRIVATE_KEY` is the private key array for the Solana Devnet wallet
that signs the source-chain burn transaction.
* `EVM_PRIVATE_KEY` is used to derive the Arc recipient address. The direct-mint
path also uses this key to submit the destination mint on Arc.
Open `.env` in your editor rather than writing values with shell commands, and
add `.env` to your `.gitignore`. This prevents credentials from leaking into
your shell history or version control.
The `npm run start` command loads variables from `.env` using Node.js native
env-file support.
This example uses one or more private keys for local testing. In production,
use a secure key management solution and never expose or share private keys.
## Step 2: Configure the script
Define the configuration constants for interacting with Solana and Arc Testnet.
### 2.1. Setup chains and wallets
The script predefines the program addresses, transfer amount, and other
parameters:
```ts TypeScript expandable theme={null}
// Solana Configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
Uint8Array.from(solanaPrivateKey),
);
// Solana CCTP Program Addresses (Devnet)
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const MESSAGE_TRANSMITTER_PROGRAM = address(
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);
// Arc Testnet Configuration
const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!;
const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const arcClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: ethAccount,
});
// Transfer Parameters
const AMOUNT = 1_000_000n;
const DESTINATION_DOMAIN = 26;
const ARC_DESTINATION_ADDRESS = ethAccount.address;
const MAX_FEE = 500n;
```
## Step 3: Implement the transfer logic
The following sections outline the core transfer logic from Solana to Arc.
For simplicity, this quickstart uses the same Arc wallet as the recipient and,
in the direct-mint path, the wallet that submits `receiveMessage`. In
production, these can be different addresses.
In the two examples provided, the path diverges at the Solana burn instruction:
* **Direct mint** uses `deposit_for_burn`, then retrieves an attestation and
calls `receiveMessage` on Arc.
* **Forwarding Service** uses `deposit_for_burn_with_hook`, then lets Circle
handle the destination-side mint on Arc.
### 3.1. Get forwarding fees and calculate the burn amount
Before you burn USDC with the
[Forwarding Service](/cctp/concepts/forwarding-service), query the CCTP fee
endpoint with `forward=true`. The forwarding fee is dynamic, so fetch it
immediately before the transfer. The returned `maxFee` must cover both the CCTP
protocol fee and the forwarding fee.
```ts TypeScript theme={null}
const FORWARDING_SERVICE_HOOK_DATA = Buffer.from(
"636374702d666f72776172640000000000000000000000000000000000000000",
"hex",
);
async function getForwardingFees() {
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/26?forward=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (!response.ok) {
throw new Error(`Failed to fetch fees: ${await response.text()}`);
}
return response.json();
}
async function calculateForwardingAmounts() {
const fees = await getForwardingFees();
const feeData = fees.find(
(fee: { finalityThreshold: number }) => fee.finalityThreshold === 1000,
);
if (!feeData) {
throw new Error("Fast-transfer forwarding fees not available");
}
const forwardFee = BigInt(feeData.forwardFee.med);
const protocolFee =
(AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = AMOUNT + maxFee;
return { maxFee, totalAmount };
}
```
### 3.2. Burn USDC with the Forwarding Service hook
Use `deposit_for_burn_with_hook` on Solana. The forwarding hook data tells
Circle to handle the destination-side `receiveMessage` call on Arc.
```ts TypeScript expandable theme={null}
type BurnContext = {
senderUsdcAccount: ReturnType;
senderAuthorityPda: ReturnType;
denylistPda: ReturnType;
messageTransmitter: ReturnType;
tokenMessenger: ReturnType;
remoteTokenMessenger: ReturnType;
tokenMinter: ReturnType;
localToken: ReturnType;
eventAuthority: ReturnType;
messageTransmitterEventAuthority: ReturnType;
messageSentEventAccount: Awaited>;
destAddressBytes32: Buffer;
};
async function getBurnContext(): Promise {
const addressEncoder = getAddressEncoder();
const [senderUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
const destAddressBytes32 = Buffer.concat([
Buffer.alloc(12),
Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
]);
const [senderAuthorityPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("sender_authority")],
});
const [denylistPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("denylist_account"),
addressEncoder.encode(solanaKeypair.address),
],
});
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const messageSentEventAccount = await generateKeyPairSigner();
return {
senderUsdcAccount,
senderAuthorityPda,
denylistPda,
messageTransmitter,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
eventAuthority,
messageTransmitterEventAuthority,
messageSentEventAccount,
destAddressBytes32,
};
}
async function burnUSDCWithForwarding() {
console.log("Burning USDC on Solana with Forwarding Service...");
const { maxFee, totalAmount } = await calculateForwardingAmounts();
const burnContext = await getBurnContext();
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(totalAmount);
const domainBuffer = Buffer.alloc(4);
domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);
const maxFeeBuffer = Buffer.alloc(8);
maxFeeBuffer.writeBigUInt64LE(maxFee);
const finalityBuffer = Buffer.alloc(4);
finalityBuffer.writeUInt32LE(1000);
const hookLengthBuffer = Buffer.alloc(4);
hookLengthBuffer.writeUInt32LE(FORWARDING_SERVICE_HOOK_DATA.length);
const instructionData = new Uint8Array(
Buffer.concat([
Buffer.from([111, 245, 62, 131, 204, 108, 223, 155]),
amountBuffer,
domainBuffer,
burnContext.destAddressBytes32,
Buffer.alloc(32),
maxFeeBuffer,
finalityBuffer,
hookLengthBuffer,
FORWARDING_SERVICE_HOOK_DATA,
]),
);
const depositForBurnIx = {
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: burnContext.senderAuthorityPda, role: 0 },
{ address: burnContext.senderUsdcAccount, role: 1 },
{ address: burnContext.denylistPda, role: 0 },
{ address: burnContext.messageTransmitter, role: 1 },
{ address: burnContext.tokenMessenger, role: 0 },
{ address: burnContext.remoteTokenMessenger, role: 0 },
{ address: burnContext.tokenMinter, role: 0 },
{ address: burnContext.localToken, role: 1 },
{ address: USDC_MINT, role: 1 },
{
address: burnContext.messageSentEventAccount.address,
role: 3,
signer: burnContext.messageSentEventAccount,
},
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: burnContext.eventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: burnContext.messageTransmitterEventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
await sendAndConfirmTransaction(signedTransaction as any, {
commitment: "confirmed",
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Burn transaction signature: ${signature}`);
return signature;
}
```
### 3.3. Verify the forwarded mint
After the burn is confirmed, poll the Iris API until it returns a
`forwardTxHash`. That hash is the Arc destination mint transaction submitted by
Circle. In the forwarding path, `forwardTxHash` is the completion signal for the
destination-side mint. You do not need to retrieve an attestation and call
`receiveMessage` yourself.
```ts TypeScript theme={null}
async function waitForForwardedMint(transactionSignature: string) {
console.log("Waiting for Forwarding Service to mint on Arc...");
while (true) {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`,
{ method: "GET" },
);
if (!response.ok) {
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = await response.json();
const forwardTxHash = data?.messages?.[0]?.forwardTxHash;
if (forwardTxHash) {
console.log(`Forwarded Mint Tx: ${forwardTxHash}`);
return forwardTxHash;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
```
### 3.1. Burn USDC on Solana
Call the `depositForBurn` instruction from the `TokenMessengerMinterV2` program
to burn USDC on Solana:
```ts TypeScript expandable theme={null}
const DIRECT_MINT_DISCRIMINATOR = Buffer.from([
215, 60, 61, 46, 114, 55, 128, 176,
]);
async function burnUSDCOnSolana() {
console.log("Burning USDC on Solana...");
const addressEncoder = getAddressEncoder();
// Get the sender's USDC token account (Associated Token Account PDA)
const [senderUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
const destAddressBytes32 = Buffer.concat([
Buffer.alloc(12),
Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
]);
// Derive PDAs (Program Derived Addresses)
const [senderAuthorityPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("sender_authority")],
});
const [denylistPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("denylist_account"),
addressEncoder.encode(solanaKeypair.address),
],
});
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
// NOTE: Domain is converted to string for PDA derivation in V2
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
// Derive event authority PDAs for Anchor CPI events
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const messageSentEventAccount = await generateKeyPairSigner();
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(AMOUNT);
const domainBuffer = Buffer.alloc(4);
domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);
const maxFeeBuffer = Buffer.alloc(8);
maxFeeBuffer.writeBigUInt64LE(MAX_FEE);
const finalityBuffer = Buffer.alloc(4);
finalityBuffer.writeUInt32LE(1000);
const instructionData = new Uint8Array(
Buffer.concat([
DIRECT_MINT_DISCRIMINATOR,
amountBuffer,
domainBuffer,
destAddressBytes32,
Buffer.alloc(32),
maxFeeBuffer,
finalityBuffer,
]),
);
const depositForBurnIx = {
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: senderAuthorityPda, role: 0 },
{ address: senderUsdcAccount, role: 1 },
{ address: denylistPda, role: 0 },
{ address: messageTransmitter, role: 1 },
{ address: tokenMessenger, role: 0 },
{ address: remoteTokenMessenger, role: 0 },
{ address: tokenMinter, role: 0 },
{ address: localToken, role: 1 },
{ address: USDC_MINT, role: 1 },
{
address: messageSentEventAccount.address,
role: 3,
signer: messageSentEventAccount,
},
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: eventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: messageTransmitterEventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
await sendAndConfirmTransaction(signedTransaction as any, {
commitment: "confirmed",
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Burn transaction signature: ${signature}`);
return signature;
}
```
### 3.2. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling
Circle's attestation API:
```ts TypeScript theme={null}
async function retrieveAttestation(transactionSignature: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
```
### 3.3. Mint USDC on Arc Testnet
Call the `receiveMessage` function from the `MessageTransmitterV2` contract on
Arc Testnet to mint USDC:
```ts TypeScript theme={null}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc testnet...");
const mintTx = await arcClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
console.log(`Mint transaction hash: ${mintTx}`);
}
```
## Step 4: Complete script
Create a `index.ts` file in your project directory and populate it with the
complete code below for the path you want to test.
```ts index.ts expandable theme={null}
import {
address,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getAddressEncoder,
getProgramDerivedAddress,
getSignatureFromTransaction,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { privateKeyToAccount } from "viem/accounts";
type FeeQuote = {
finalityThreshold: number;
minimumFee: number;
forwardFee: { med: number };
};
type BurnContext = {
senderUsdcAccount: ReturnType;
senderAuthorityPda: ReturnType;
denylistPda: ReturnType;
messageTransmitter: ReturnType;
tokenMessenger: ReturnType;
remoteTokenMessenger: ReturnType;
tokenMinter: ReturnType;
localToken: ReturnType;
eventAuthority: ReturnType;
messageTransmitterEventAuthority: ReturnType;
messageSentEventAccount: Awaited>;
destAddressBytes32: Buffer;
};
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
Uint8Array.from(solanaPrivateKey),
);
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const MESSAGE_TRANSMITTER_PROGRAM = address(
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);
const ethAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY! as `0x${string}`,
);
const AMOUNT = 1_000_000n;
const DESTINATION_DOMAIN = 26;
const ARC_DESTINATION_ADDRESS = ethAccount.address;
const FORWARDING_SERVICE_HOOK_DATA = Buffer.from(
"636374702d666f72776172640000000000000000000000000000000000000000",
"hex",
);
const FORWARDING_DISCRIMINATOR = Buffer.from([
111, 245, 62, 131, 204, 108, 223, 155,
]);
async function getForwardingFeeQuote() {
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/5/26?forward=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
if (!response.ok) {
throw new Error(`Failed to fetch fees: ${await response.text()}`);
}
const fees = (await response.json()) as FeeQuote[];
const feeData = fees.find((fee) => fee.finalityThreshold === 1000);
if (!feeData) {
throw new Error("Fast-transfer forwarding fees not available");
}
return feeData;
}
async function calculateForwardingAmounts() {
const feeData = await getForwardingFeeQuote();
const forwardFee = BigInt(feeData.forwardFee.med);
const protocolFee =
(AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = AMOUNT + maxFee;
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
return { maxFee, totalAmount };
}
async function getBurnContext(): Promise {
const addressEncoder = getAddressEncoder();
const [senderUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
const destAddressBytes32 = Buffer.concat([
Buffer.alloc(12),
Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
]);
const [senderAuthorityPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("sender_authority")],
});
const [denylistPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("denylist_account"),
addressEncoder.encode(solanaKeypair.address),
],
});
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const messageSentEventAccount = await generateKeyPairSigner();
return {
senderUsdcAccount,
senderAuthorityPda,
denylistPda,
messageTransmitter,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
eventAuthority,
messageTransmitterEventAuthority,
messageSentEventAccount,
destAddressBytes32,
};
}
async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) {
console.log("Burning USDC on Solana with Forwarding Service...");
const burnContext = await getBurnContext();
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(totalAmount);
const domainBuffer = Buffer.alloc(4);
domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);
const maxFeeBuffer = Buffer.alloc(8);
maxFeeBuffer.writeBigUInt64LE(maxFee);
const finalityBuffer = Buffer.alloc(4);
finalityBuffer.writeUInt32LE(1000);
const hookLengthBuffer = Buffer.alloc(4);
hookLengthBuffer.writeUInt32LE(FORWARDING_SERVICE_HOOK_DATA.length);
const instructionData = new Uint8Array(
Buffer.concat([
FORWARDING_DISCRIMINATOR,
amountBuffer,
domainBuffer,
burnContext.destAddressBytes32,
Buffer.alloc(32),
maxFeeBuffer,
finalityBuffer,
hookLengthBuffer,
FORWARDING_SERVICE_HOOK_DATA,
]),
);
const depositForBurnIx = {
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: burnContext.senderAuthorityPda, role: 0 },
{ address: burnContext.senderUsdcAccount, role: 1 },
{ address: burnContext.denylistPda, role: 0 },
{ address: burnContext.messageTransmitter, role: 1 },
{ address: burnContext.tokenMessenger, role: 0 },
{ address: burnContext.remoteTokenMessenger, role: 0 },
{ address: burnContext.tokenMinter, role: 0 },
{ address: burnContext.localToken, role: 1 },
{ address: USDC_MINT, role: 1 },
{
address: burnContext.messageSentEventAccount.address,
role: 3,
signer: burnContext.messageSentEventAccount,
},
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: burnContext.eventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: burnContext.messageTransmitterEventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
await sendAndConfirmTransaction(signedTransaction as any, {
commitment: "confirmed",
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Burn transaction signature: ${signature}`);
return signature;
}
async function waitForForwardedMint(transactionSignature: string) {
console.log("Waiting for Forwarding Service to mint on Arc...");
while (true) {
const response = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`,
{ method: "GET" },
);
if (!response.ok) {
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = await response.json();
const forwardTxHash = data?.messages?.[0]?.forwardTxHash;
if (forwardTxHash) {
console.log(`Forwarded Mint Tx: ${forwardTxHash}`);
return forwardTxHash;
}
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
async function main() {
console.log("Solana address:", solanaKeypair.address);
console.log("Arc recipient:", ARC_DESTINATION_ADDRESS);
// [1] Quote forwarding fees and derive the total source-chain burn amount.
const { maxFee, totalAmount } = await calculateForwardingAmounts();
// [2] Burn on Solana with the forwarding hook enabled.
const burnSignature = await burnUSDCWithForwarding(totalAmount, maxFee);
// [3] Poll until Iris returns the destination mint transaction hash.
await waitForForwardedMint(burnSignature);
console.log(
"USDC transfer from Solana Devnet to Arc Testnet completed with Forwarding Service.",
);
}
main().catch(console.error);
```
```ts index.ts expandable theme={null}
import {
address,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getAddressEncoder,
getProgramDerivedAddress,
getSignatureFromTransaction,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { createWalletClient, http, encodeFunctionData } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
Uint8Array.from(solanaPrivateKey),
);
// Solana CCTP Program Addresses (Devnet)
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
"CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const MESSAGE_TRANSMITTER_PROGRAM = address(
"CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);
const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!;
const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const arcClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: ethAccount,
});
const AMOUNT = 1_000_000n;
const DESTINATION_DOMAIN = 26;
const ARC_DESTINATION_ADDRESS = ethAccount.address;
const MAX_FEE = 500n;
const DIRECT_MINT_DISCRIMINATOR = Buffer.from([
215, 60, 61, 46, 114, 55, 128, 176,
]);
async function burnUSDCOnSolana() {
console.log("Burning USDC on Solana...");
const addressEncoder = getAddressEncoder();
const [senderUsdcAccount] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM,
seeds: [
addressEncoder.encode(solanaKeypair.address),
addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
addressEncoder.encode(USDC_MINT),
],
});
const destAddressBytes32 = Buffer.concat([
Buffer.alloc(12),
Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
]);
const [senderAuthorityPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("sender_authority")],
});
const [denylistPda] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("denylist_account"),
addressEncoder.encode(solanaKeypair.address),
],
});
const [messageTransmitter] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("message_transmitter")],
});
const [tokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_messenger")],
});
const [remoteTokenMessenger] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("remote_token_messenger"),
new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
],
});
const [tokenMinter] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("token_minter")],
});
const [localToken] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [
new TextEncoder().encode("local_token"),
addressEncoder.encode(USDC_MINT),
],
});
const [eventAuthority] = await getProgramDerivedAddress({
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
programAddress: MESSAGE_TRANSMITTER_PROGRAM,
seeds: [new TextEncoder().encode("__event_authority")],
});
const messageSentEventAccount = await generateKeyPairSigner();
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(AMOUNT);
const domainBuffer = Buffer.alloc(4);
domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);
const maxFeeBuffer = Buffer.alloc(8);
maxFeeBuffer.writeBigUInt64LE(MAX_FEE);
const finalityBuffer = Buffer.alloc(4);
finalityBuffer.writeUInt32LE(1000);
const instructionData = new Uint8Array(
Buffer.concat([
DIRECT_MINT_DISCRIMINATOR,
amountBuffer,
domainBuffer,
destAddressBytes32,
Buffer.alloc(32),
maxFeeBuffer,
finalityBuffer,
]),
);
const depositForBurnIx = {
programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
accounts: [
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: solanaKeypair.address, role: 3, signer: solanaKeypair },
{ address: senderAuthorityPda, role: 0 },
{ address: senderUsdcAccount, role: 1 },
{ address: denylistPda, role: 0 },
{ address: messageTransmitter, role: 1 },
{ address: tokenMessenger, role: 0 },
{ address: remoteTokenMessenger, role: 0 },
{ address: tokenMinter, role: 0 },
{ address: localToken, role: 1 },
{ address: USDC_MINT, role: 1 },
{
address: messageSentEventAccount.address,
role: 3,
signer: messageSentEventAccount,
},
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: TOKEN_PROGRAM_ADDRESS, role: 0 },
{ address: SYSTEM_PROGRAM_ADDRESS, role: 0 },
{ address: eventAuthority, role: 0 },
{ address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 },
{ address: messageTransmitterEventAuthority, role: 0 },
{ address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 },
],
data: instructionData,
};
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
rpc,
rpcSubscriptions,
});
await sendAndConfirmTransaction(signedTransaction as any, {
commitment: "confirmed",
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Burn transaction signature: ${signature}`);
return signature;
}
async function retrieveAttestation(transactionSignature: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${
text ? ` - ${text}` : ""
}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc testnet...");
const mintTx = await arcClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
console.log(`Mint transaction hash: ${mintTx}`);
}
async function main() {
// [1] Burn USDC on Solana Devnet.
const burnSignature = await burnUSDCOnSolana();
// [2] Poll until Iris returns a complete attestation.
const attestation = await retrieveAttestation(burnSignature);
// [3] Submit the destination-side mint on Arc Testnet.
await mintUSDCOnArc(attestation);
console.log("USDC transfer from Solana Devnet to Arc Testnet completed.");
}
main().catch(console.error);
```
## Step 5: Test the script
Run the following command to execute the script:
```shell Shell theme={null}
npm run start
```
Once the script runs and the transfer is finalized, a confirmation message is
logged in the console.
**Rate limit:** The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.
# Transfer USDC to and from Stellar
Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc
Build scripts to transfer USDC between Arc Testnet and Stellar Testnet using CCTP
Use CCTP to transfer USDC with Stellar Testnet as the source or destination.
On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. Before you integrate beyond these examples, read
[CCTP on Stellar](/cctp/references/stellar).
Pick the tab that matches the direction of your transfer.
This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc
Testnet using CCTP. You use the
[@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) library to
interact with Stellar Soroban contracts, and [`viem`](https://viem.sh/) to mint
USDC on Arc Testnet. When you finish, you will have executed a full
burn-attest-mint flow.
You should be comfortable using a terminal and Node.js. Familiarity with Stellar
Soroban transactions and basic EVM usage helps you follow and adapt the script.
Examples use Arc Testnet as the destination, but you can use any
[supported blockchain](/cctp/concepts/supported-chains-and-domains).
## Prerequisites
Before you begin this tutorial, ensure you have:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM wallet with the private key available for Arc Testnet
* Added the Arc Testnet network to your wallet
([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
* Funded your wallet with Arc Testnet USDC (for gas fees) from the
[Circle Faucet](https://faucet.circle.com)
* Prepared a Stellar Testnet wallet with the secret key (`S...`) available
* Funded your Stellar wallet with testnet XLM from the
[Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees
on Stellar Testnet)
* Established a
[USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) on
your Stellar account so you can hold the testnet USDC you burn
* Funded your Stellar wallet with Stellar Testnet USDC from the
[Circle Faucet](https://faucet.circle.com)
You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund
accounts and establish USDC trustlines.
## Step 1: Set up the project
This step shows you how to prepare your project and environment.
### 1.1. Set up your development environment
Create a new directory and install the required dependencies:
```bash Shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir cctp-stellar-to-arc
cd cctp-stellar-to-arc
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install @stellar/stellar-sdk viem
# Install dev dependencies
npm install --save-dev typescript @types/node
```
### 1.2. Initialize and configure the project
This step is optional. It helps prevent missing types in your IDE or editor.
Create a `tsconfig.json` file:
```shell theme={null}
npx tsc --init
```
Then, update the `tsconfig.json` file:
```shell theme={null}
cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
```
### 1.3. Configure environment variables
Open .env in your editor and add:
```text theme={null}
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
```
* `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
transactions on Stellar Testnet.
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc
Testnet.
Open `.env` in your editor rather than writing values with shell commands, and
add `.env` to your `.gitignore`. This prevents credentials from leaking into
your shell history or version control.
The `npm run start` command loads variables from `.env` using Node.js native
env-file support.
This example uses one or more private keys for local testing. In production,
use a secure key management solution and never expose or share private keys.
## Step 2: Configure the script
Define contract addresses, amounts, and clients for Stellar Testnet and Arc
Testnet.
### 2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and maximum fee.
```ts TypeScript theme={null}
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
pad,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// Contract Addresses
const STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Chain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
// Authentication
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
```
### 2.2. Set up wallet clients
The wallet client configures the appropriate network settings using `viem`. In
this example, the script connects to Arc Testnet.
```ts TypeScript theme={null}
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const arcWalletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
```
### 2.3. Add helper function
The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban
contract transaction.
```ts TypeScript theme={null}
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
) {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
```
## Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Stellar, poll
for an attestation, then mint on Arc. A successful run prints transaction hashes
and a completion message in the console.
### 3.1. Approve USDC on Stellar
Approve the `TokenMessengerMinterV2` contract to spend your USDC. The
`submitSorobanTx` helper is used to submit the approve call to the Stellar USDC
contract.
```ts TypeScript theme={null}
async function approveUSDC() {
console.log("Approving USDC spend on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(`Approve Tx: ${approveHash}`);
}
```
### 3.2. Burn USDC on Stellar
Call `deposit_for_burn` to burn USDC on Stellar. The `submitSorobanTx` helper is
used to submit the burn call with the transfer parameters listed below:
* **Burn amount**: The amount of USDC to burn (in Stellar subunits, 7 decimals)
* **Destination domain**: The target blockchain for minting USDC (see
[supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
* **Mint recipient**: The wallet address that receives the minted USDC on Arc
* **Burn token**: The contract address of the USDC token on Stellar
* **Destination caller**: The address on the target blockchain that may call
`receiveMessage`
* **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
(in Stellar subunits, 7 decimals)
* **Finality threshold**: Determines whether it's a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
(1000 or less) or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
(2000 or more)
```ts TypeScript theme={null}
async function burnUSDC() {
console.log("Burning USDC on Stellar...");
// Bytes32 Formatted Parameters
const evmAddressBytes32 = pad(evmAccount.address);
const mintRecipient = xdr.ScVal.scvBytes(
Buffer.from(evmAddressBytes32.slice(2), "hex"),
);
const server = new rpc.Server(STELLAR_RPC_URL);
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
mintRecipient,
new Address(STELLAR_USDC).toScVal(),
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
nativeToScVal(MAX_FEE, { type: "i128" }),
nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
],
);
console.log(`Burn Tx: ${txHash}`);
return txHash;
}
```
### 3.3. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling
Circle's attestation API.
* Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2)
API endpoint to retrieve the attestation.
* Pass `STELLAR_DOMAIN` for the `sourceDomain` path parameter, using the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for Stellar Testnet (27).
* Pass `transactionHash` from the value returned by `submitSorobanTx` within the
`burnUSDC` function above.
```ts TypeScript theme={null}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
```
### 3.4. Mint USDC on Arc
Call the
[`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage)
from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses)
deployed on Arc Testnet to mint USDC on the destination blockchain.
* Pass the signed attestation and the message bytes as parameters.
* The contract verifies the attestation and mints USDC to the recipient encoded
in the CCTP message.
```ts TypeScript theme={null}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash });
console.log(`Mint Tx: ${hash}`);
}
```
## Step 4: Full script
Create an `index.ts` file in your project directory and paste the full script
below so you can run the flow from one file.
```ts index.ts expandable theme={null}
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
pad,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// ============ Configuration Constants ============
// Contract Addresses
const STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Chain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
// Authentication
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
// Set up wallet clients
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const arcWalletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
) {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
// ============ CCTP Flow Functions ============
async function approveUSDC() {
console.log("Approving USDC spend on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(`Approve Tx: ${approveHash}`);
}
async function burnUSDC() {
console.log("Burning USDC on Stellar...");
// Bytes32 Formatted Parameters
const evmAddressBytes32 = pad(evmAccount.address);
const mintRecipient = xdr.ScVal.scvBytes(
Buffer.from(evmAddressBytes32.slice(2), "hex"),
);
const server = new rpc.Server(STELLAR_RPC_URL);
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
mintRecipient,
new Address(STELLAR_USDC).toScVal(),
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
nativeToScVal(MAX_FEE, { type: "i128" }),
nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
],
);
console.log(`Burn Tx: ${txHash}`);
return txHash;
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash });
console.log(`Mint Tx: ${hash}`);
}
// ============ Main Execution ============
async function main() {
await approveUSDC();
const burnTx = await burnUSDC();
const attestation = await retrieveAttestation(burnTx);
await mintUSDCOnArc(attestation);
console.log("USDC transfer from Stellar to Arc completed!");
}
main().catch(console.error);
```
## Step 5: Test the script
Run the following command to execute the script:
```shell Shell theme={null}
npm run start
```
When the transfer finishes, the console logs a completion message and the
relevant transaction hashes. Successful output looks similar to the following:
```bash Shell theme={null}
Approving USDC spend on Stellar...
Approve Tx:
Burning USDC on Stellar...
Burn Tx:
Retrieving attestation...
Waiting for attestation...
Waiting for attestation...
Attestation retrieved successfully!
Minting USDC on Arc Testnet...
Mint Tx: 0x...
USDC transfer from Stellar to Arc completed!
```
Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 5 seconds with no
timeout, so if it appears to hang at `Waiting for attestation...`, allow at
least five minutes before investigating.
**Rate limit:** The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.
This quickstart demonstrates how to transfer USDC from Arc Testnet to Stellar
Testnet using CCTP. You use the [viem](https://viem.sh/) library to burn USDC on
Arc, and [`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) to
mint and forward tokens on the Stellar CCTP Forwarder. When you finish, you will
have executed a full burn-attest-mint flow.
You should be comfortable using a terminal and Node.js. Familiarity with basic
EVM usage and Stellar Soroban transactions helps you follow and adapt the
script. Examples use Arc Testnet as the source, but you can use any
[supported blockchain](/cctp/concepts/supported-chains-and-domains).
Always
[use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
`destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
## Prerequisites
Before you begin this tutorial, ensure you have:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM wallet with the private key available for Arc Testnet
* Added the Arc Testnet network to your wallet
([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
* Funded your wallet with Arc Testnet USDC (for gas fees and the transfer
amount) from the [Circle Faucet](https://faucet.circle.com)
* Prepared a Stellar Testnet wallet with the secret key (`S...`) available
* Funded your Stellar wallet with testnet XLM from the
[Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees
on Stellar Testnet)
* If needed, identified the forward recipient Stellar `strkey` (`G...`, `C...`,
or `M...`). For `G...` or `M...` recipients, established a
[USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) before
receiving minted USDC
You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund
accounts and establish USDC trustlines.
## Step 1: Set up the project
This step shows you how to prepare your project and environment.
### 1.1. Set up your development environment
Create a new directory and install the required dependencies:
```bash Shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir cctp-arc-to-stellar
cd cctp-arc-to-stellar
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"
# Install runtime dependencies
npm install @stellar/stellar-sdk viem
# Install dev dependencies
npm install --save-dev typescript @types/node
```
### 1.2. Initialize and configure the project
This step is optional. It helps prevent missing types in your IDE or editor.
Create a `tsconfig.json` file:
```shell theme={null}
npx tsc --init
```
Then, update the `tsconfig.json` file:
```shell theme={null}
cat <<'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["node"]
}
}
EOF
```
### 1.3. Configure environment variables
Open `.env` in your editor and add:
```text theme={null}
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
# FORWARD_RECIPIENT=G_OR_C_OR_M_STELLAR_STRKEY
```
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc
Testnet.
* `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
transactions on Stellar Testnet.
* `FORWARD_RECIPIENT` is required when the final recipient is a `G...` or `M...`
address. When omitted, the script defaults to the public key derived from
`STELLAR_SECRET_KEY`.
The `npm run start` command loads variables from `.env` using Node.js native
env-file support.
This example uses one or more private keys for local testing. In production,
use a secure key management solution and never expose or share private keys.
## Step 2: Configure the script
This section covers the necessary setup for the transfer script, including
defining keys and addresses, and configuring the wallet clients for interacting
with Arc and Stellar.
### 2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and other
parameters.
```ts TypeScript theme={null}
import {
Contract,
Keypair,
StrKey,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// ============ Configuration Constants ============
// Contract Addresses
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const STELLAR_CCTP_FORWARDER =
"CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";
// Transfer Parameters
const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
const MAX_FEE = 500n; // 0.0005 USDC (500 subunits)
// Chain-specific Parameters
const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
```
### 2.2. Set up wallet clients
The wallet clients configure the appropriate network settings using `viem`. In
this example, the script connects to Arc Testnet.
```ts TypeScript theme={null}
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const arcWalletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
```
### 2.3. Encode the CCTP Forwarder hook data
When transferring to Stellar, the Arc burn encodes the forward recipient in the
`hookData` field. The Stellar CCTP Forwarder contract is set as both the
`mintRecipient` and `destinationCaller`.
The hook data encodes the forward recipient in a specific binary format:
```text Byte layout theme={null}
[32-byte header + forward recipient bytes]
├─ Bytes 0-23: Zero padding (0x000...000)
├─ Bytes 24-27: Hook version (uint32, currently 0)
├─ Bytes 28-31: Forward recipient strkey length (uint32, byte length)
└─ Bytes 32+: Forward recipient strkey as UTF-8 bytes
```
Example: For forward recipient "GABC...XYZ" (56 chars):
* Zero padding: 0x000000000000000000000000000000000000000000000000
* Hook version: 0x00000000
* Length: 0x00000038 (56 in hex)
* Forward recipient: "GABC...XYZ" encoded as UTF-8
This encoding tells the CCTP Forwarder where to send tokens after minting.
`G` and `M` strkey forward recipients need an established
[USDC trustline](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines)
before receiving funds. Transfers without a trustline fail.
This path follows the
[CCTP Forwarder](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
pattern:
1. **Burn with hook**: `depositForBurnWithHook` on the Arc
[`TokenMessengerV2`](/cctp/references/contract-interfaces#depositforburnwithhook)
contract. `mintRecipient` is the Stellar
[CCTP Forwarder](/cctp/references/stellar-contracts#cctpforwarder), and
`hookData` carries the forward recipient strkey.
2. **Attest**: Circle's attestation service signs the burn event.
3. **Mint and forward**: `mint_and_forward` on the Stellar CCTP Forwarder calls
`receive_message` on `MessageTransmitter`, mints tokens, and forwards them
per the hook data.
The CCTP Forwarder flow is non-custodial. In one atomic Soroban transaction,
`mint_and_forward` mints to `CctpForwarder` and pays `forwardRecipient` onchain.
Circle does not take custody of the minted balance in between.
```ts TypeScript theme={null}
// Convert a Stellar contract address (C...) to 0x-prefixed bytes32
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
// Build hook data encoding the forward recipient
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24); // hook version = 0
hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
return `0x${hookData.toString("hex")}`;
}
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
// Falls back to the Stellar public key derived from STELLAR_SECRET_KEY
// when FORWARD_RECIPIENT is unset or empty.
const forwardRecipient =
process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey();
const hookData = buildCctpForwarderHookData(forwardRecipient);
```
## Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Arc, poll for
an attestation, then mint and forward on Stellar. A successful run prints
transaction hashes and a completion message in the console.
### 3.1. Approve USDC on Arc
Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) to withdraw
USDC from your wallet. This allows the contract to burn USDC when you initiate
the transfer.
```ts TypeScript theme={null}
async function approveUSDC() {
console.log("Approving USDC spend on Arc...");
const approveTx = await arcWalletClient.sendTransaction({
to: ARC_USDC as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: approveTx });
console.log(`Approve Tx: ${approveTx}`);
}
```
### 3.2. Burn USDC on Arc
Call `depositForBurnWithHook` to burn USDC with the CCTP Forwarder hook data.
You specify the following parameters:
* **Burn amount**: The amount of USDC to burn (in Arc subunits, 6 decimals)
* **Destination domain**: The target blockchain for minting USDC (see
[supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
* **Mint recipient**: The CCTP Forwarder contract address on Stellar (encoded as
`bytes32`)
* **Burn token**: The contract address of the USDC token being burned on Arc
* **Destination caller**: The CCTP Forwarder contract address on Stellar
(restricts who can call `receive_message`)
* **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
* **Finality threshold**: Determines whether it's a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
(1000 or less) or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
(2000 or more)
* **Hook data**: Encodes the final Stellar recipient address for the CCTP
Forwarder
Always
[use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
`destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
`mintRecipient` and `destinationCaller` must be the Stellar CCTP Forwarder
contract address. `TokenMessengerMinter` assumes `mintRecipient` is a contract
address, so this example validates `STELLAR_CCTP_FORWARDER` as a contract
`strkey` and uses the final recipient only in `hookData`.
```ts TypeScript theme={null}
async function burnUSDC() {
console.log("Burning USDC on Arc (with hook)...");
const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER);
const burnTx = await arcWalletClient.sendTransaction({
to: ARC_TOKEN_MESSENGER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
AMOUNT,
STELLAR_DOMAIN,
cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder
ARC_USDC as `0x${string}`,
cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder
MAX_FEE,
2000, // Standard Transfer finality threshold
hookData,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: burnTx });
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
```
### 3.3. Retrieve attestation
Retrieve the attestation required to complete the CCTP transfer by calling
Circle's attestation API.
* Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2)
API endpoint to retrieve the attestation.
* Pass `ARC_TESTNET_DOMAIN` for the `sourceDomain` path parameter, using the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for Arc Testnet (26).
* Pass `transactionHash` from the value returned by `burnUSDC` above.
```ts TypeScript theme={null}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
```
### 3.4. Mint and forward USDC on Stellar
The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban
contract transaction.
```ts TypeScript theme={null}
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
) {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
```
Use the `submitSorobanTx` helper to call `mint_and_forward` on the Stellar CCTP
Forwarder. This verifies the CCTP message and attestation, mints USDC through
the `TokenMessengerMinter`, and forwards it to the recipient encoded in the hook
data:
```ts TypeScript theme={null}
async function mintAndForwardOnStellar(attestation: AttestationMessage) {
console.log("Minting and forwarding USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const messageBytes = Buffer.from(
attestation.message.replace("0x", ""),
"hex",
);
const attestationBytes = Buffer.from(
attestation.attestation.replace("0x", ""),
"hex",
);
const txHash = await submitSorobanTx(
server,
STELLAR_CCTP_FORWARDER,
"mint_and_forward",
[xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
);
console.log(`mint_and_forward Tx: ${txHash}`);
}
```
## Step 4: Full script
Create an `index.ts` file in your project directory and paste the full script
below so you can run the flow from one file.
```ts index.ts expandable theme={null}
import {
Contract,
Keypair,
StrKey,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// ============ Configuration Constants ============
// Contract Addresses
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const STELLAR_CCTP_FORWARDER =
"CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";
// Transfer Parameters
const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
const MAX_FEE = 500n; // 0.0005 USDC (500 subunits)
// Chain-specific Parameters
const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
// Set up wallet clients
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const arcWalletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
// Hook Data — encodes the forward recipient for the CCTP Forwarder contract
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24);
hookData.writeUInt32BE(recipientBytes.length, 28);
recipientBytes.copy(hookData, 32);
return `0x${hookData.toString("hex")}`;
}
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
// Falls back to the Stellar public key derived from STELLAR_SECRET_KEY
// when FORWARD_RECIPIENT is unset or empty.
const forwardRecipient =
process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey();
const hookData = buildCctpForwarderHookData(forwardRecipient);
// ============ CCTP Flow Functions ============
async function approveUSDC() {
console.log("Approving USDC spend on Arc...");
const approveTx = await arcWalletClient.sendTransaction({
to: ARC_USDC as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: approveTx });
console.log(`Approve Tx: ${approveTx}`);
}
async function burnUSDC() {
console.log("Burning USDC on Arc (with hook)...");
const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER);
const burnTx = await arcWalletClient.sendTransaction({
to: ARC_TOKEN_MESSENGER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
AMOUNT,
STELLAR_DOMAIN,
cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder
ARC_USDC as `0x${string}`,
cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder
MAX_FEE,
2000, // Standard Transfer finality threshold
hookData,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: burnTx });
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
) {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
async function mintAndForwardOnStellar(attestation: AttestationMessage) {
console.log("Minting and forwarding USDC on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const messageBytes = Buffer.from(
attestation.message.replace("0x", ""),
"hex",
);
const attestationBytes = Buffer.from(
attestation.attestation.replace("0x", ""),
"hex",
);
const txHash = await submitSorobanTx(
server,
STELLAR_CCTP_FORWARDER,
"mint_and_forward",
[xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
);
console.log(`mint_and_forward Tx: ${txHash}`);
}
// ============ Main Execution ============
async function main() {
await approveUSDC();
const burnTx = await burnUSDC();
const attestation = await retrieveAttestation(burnTx);
await mintAndForwardOnStellar(attestation);
console.log("USDC transfer from Arc to Stellar completed!");
}
main().catch(console.error);
```
## Step 5: Test the script
Run the following command to execute the script:
```shell Shell theme={null}
npm run start
```
When the transfer finishes, the console logs a completion message and the
relevant transaction hashes. Successful output looks similar to the following:
```bash Shell theme={null}
Approving USDC spend on Arc...
Approve Tx: 0x65d6504333ac76cf952975dad29d4a31d8de28c59d7c97ee8ac5d0c360c0e70c
Burning USDC on Arc (with hook)...
Burn Tx: 0x73e1eb9224ca5778be763b4e8afc11cfa63e7ae3caa8b2748ce73f4f3d07181a
Retrieving attestation...
Waiting for attestation...
Attestation retrieved successfully!
Minting and forwarding USDC on Stellar...
mint_and_forward Tx:
USDC transfer from Arc to Stellar completed!
```
Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 5 seconds with no
timeout, so if it appears to hang at `Waiting for attestation...`, allow at
least five minutes before investigating.
**Rate limit:** The attestation service rate limit is 35 requests per second. If
you exceed this limit, the service blocks all API requests for the next 5
minutes and returns an HTTP 429 (Too Many Requests) response.
# Attestation Verification
Source: https://developers.circle.com/cctp/references/attestation-verification
Technical reference for verifying CCTP attestation signatures
When you retrieve an attestation from Circle's Attestation Service, you can
optionally verify the attestation signature before using it to mint USDC on the
destination blockchain. This page explains how the verification process works
and when you might want to use it.
## How verification works
The verification process uses cryptographic signature recovery to confirm that
Circle's Attestation Service signed the message. It involves the following
steps:
Fetch Circle's current public key from the
[`GET /v2/publicKeys`](/api-reference/cctp/all/get-public-keys-v2) endpoint.
Create a `keccak256` hash of the message bytes.
Split the 65-byte attestation into its `r`, `s`, and `v` components (ECDSA
signature format).
Use the signature and message hash to recover the public key that signed the
message.
Convert both the recovered public key and Circle's public key to Ethereum
addresses and compare them.
If the addresses match, the attestation was signed by Circle's Attestation
Service and is valid.
## When to verify attestations
Attestation verification is optional because the CCTP contracts on the
destination blockchain perform their own verification when you call
`receiveMessage`. However, you might want to verify attestations before
submitting the mint transaction if:
* **Your application requires an additional layer of security**: Verifying
before minting provides defense-in-depth by catching invalid attestations at
the application layer.
* **You want to detect invalid attestations before paying gas fees**: If an
attestation is invalid, the mint transaction fails and you lose the gas fees.
Pre-verification lets you catch this before submitting the transaction.
* **You're building a relayer service that batches multiple attestations**:
Relayers can verify each attestation in a batch before submitting, preventing
a single invalid attestation from affecting the entire batch.
## Verification code example
The following examples show how to verify an attestation signature using Viem or
Ethers:
```ts Viem theme={null}
import { keccak256, hexToBytes, recoverAddress, bytesToHex } from "viem";
interface PublicKey {
publicKey: `0x${string}`;
cctpVersion: number;
}
interface AttestationData {
message: string;
attestation: string;
}
function publicKeyToAddress(publicKey: `0x${string}`): `0x${string}` {
// Remove '0x04' prefix (uncompressed public key marker)
const publicKeyWithoutPrefix = `0x${publicKey.slice(4)}` as `0x${string}`;
const hash = keccak256(hexToBytes(publicKeyWithoutPrefix));
// Take last 20 bytes (40 hex chars) as address
return `0x${hash.slice(-40)}`;
}
async function getPublicKeys() {
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/publicKeys",
);
const data = await response.json();
return data.publicKeys
.filter((key: PublicKey) => key.cctpVersion === 2)
.map((key: PublicKey) => key.publicKey);
}
async function verifyAttestation(
attestationData: AttestationData,
publicKeys: `0x${string}`[],
) {
try {
const messageHash = keccak256(attestationData.message as `0x${string}`);
const attestationBytes = hexToBytes(
attestationData.attestation as `0x${string}`,
);
const signatureLength = 65;
const numSignatures = attestationBytes.length / signatureLength;
if (attestationBytes.length % signatureLength !== 0) {
throw new Error(`Invalid attestation length: ${attestationBytes.length}`);
}
let validSignatures = 0;
for (let i = 0; i < numSignatures; i++) {
const start = i * signatureLength;
const signature = attestationBytes.slice(start, start + signatureLength);
const recoveredAddress = await recoverAddress({
hash: messageHash,
signature: bytesToHex(signature),
});
const isValid = publicKeys.some(
(publicKey) =>
publicKeyToAddress(publicKey).toLowerCase() ===
recoveredAddress.toLowerCase(),
);
if (isValid) validSignatures++;
}
const threshold = Math.ceil(publicKeys.length / 2);
console.log(
`Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`,
);
return validSignatures >= threshold;
} catch (error) {
console.error(
"Error verifying attestation:",
error instanceof Error ? error.message : String(error),
);
return false;
}
}
const attestationData: AttestationData = {
message: "0x000000010000001a00000015...", // Full message hex from API
attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API
};
// Example usage
const publicKeys = await getPublicKeys();
const isValid = await verifyAttestation(attestationData, publicKeys);
```
```ts Ethers.js theme={null}
import { ethers } from "ethers";
interface PublicKey {
publicKey: string;
cctpVersion: number;
}
interface AttestationData {
message: string;
attestation: string;
}
async function getPublicKeys() {
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/publicKeys",
);
const data = await response.json();
// Get all public keys for CCTP V2
const v2Keys = data.publicKeys
.filter((key: PublicKey) => key.cctpVersion === 2)
.map((key: PublicKey) => key.publicKey);
if (v2Keys.length === 0) {
throw new Error("CCTP V2 public key not found");
}
return v2Keys;
}
function verifyAttestation(
attestationData: AttestationData,
publicKeys: string[],
) {
try {
const messageHash = ethers.keccak256(attestationData.message);
const attestationBytes = ethers.getBytes(attestationData.attestation);
// V2 attestation has multiple 65-byte signatures
const signatureLength = 65;
const numSignatures = attestationBytes.length / signatureLength;
if (attestationBytes.length % signatureLength !== 0) {
throw new Error(`Invalid attestation length: ${attestationBytes.length}`);
}
let validSignatures = 0;
// Verify each signature
for (let i = 0; i < numSignatures; i++) {
const start = i * signatureLength;
const sigBytes = attestationBytes.slice(start, start + signatureLength);
const r = ethers.hexlify(sigBytes.slice(0, 32));
const s = ethers.hexlify(sigBytes.slice(32, 64));
const v = sigBytes[64];
const signature = { r, s, v };
const recoveredAddress = ethers.recoverAddress(messageHash, signature);
// Check if recovered address matches any V2 public key
const isValid = publicKeys.some(
(publicKey) =>
ethers.computeAddress(publicKey).toLowerCase() ===
recoveredAddress.toLowerCase(),
);
if (isValid) validSignatures++;
}
const threshold = Math.ceil(publicKeys.length / 2);
console.log(
`Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`,
);
return validSignatures >= threshold;
} catch (error) {
console.error("Error verifying attestation:", (error as Error).message);
return false;
}
}
// Use attestation data from the API
const attestationData: AttestationData = {
message: "0x000000010000001a00000015...", // Full message hex from API
attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API
};
// Example usage
const publicKeys = await getPublicKeys();
const isValid = verifyAttestation(attestationData, publicKeys);
```
# Contract Addresses
Source: https://developers.circle.com/cctp/references/contract-addresses
CCTP smart contract addresses for EVM-compatible blockchains
This page lists the deployed contract addresses for CCTP on all
[supported EVM-compatible blockchains](/cctp/concepts/supported-chains-and-domains).
For contract interfaces and method signatures, see
[Contract Interfaces](/cctp/references/contract-interfaces).
Full contract source code is
[available on GitHub](https://github.com/circlefin/evm-cctp-contracts).
For non-EVM blockchain contract addresses, see:
* [Solana Programs](/cctp/references/solana-programs)
* [Stellar Contracts](/cctp/references/stellar-contracts)
* [Starknet Contracts](/cctp/references/starknet-contracts)
## Mainnet contract addresses
### TokenMessengerV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Avalanche** | 1 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://snowtrace.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **OP Mainnet** | 2 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://optimistic.etherscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Arbitrum** | 3 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://arbiscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Base** | 6 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://basescan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Polygon PoS** | 7 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://polygonscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Unichain** | 10 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://uniscan.xyz/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Linea** | 11 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://lineascan.build/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Codex** | 12 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.codex.xyz/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Sonic** | 13 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://sonicscan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **World Chain** | 14 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://worldscan.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Monad** | 15 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://monadvision.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Sei** | 16 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://seiscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **XDC** | 18 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://xdcscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **HyperEVM** | 19 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://hyperscan.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Ink** | 21 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.inkonchain.com/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Plume** | 22 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.plume.org/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **EDGE** | 28 | [`0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c`](https://pro.edgex.exchange/en-US/explorer/address/0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c) |
| **Injective** | 29 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://blockscout.injective.network/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Morph** | 30 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://explorer.morph.network/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
| **Pharos** | 31 | [`0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d`](https://pharos.socialscan.io/address/0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d) |
### MessageTransmitterV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://etherscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Avalanche** | 1 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://snowtrace.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **OP Mainnet** | 2 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://optimistic.etherscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Arbitrum** | 3 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://arbiscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Base** | 6 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://basescan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Polygon PoS** | 7 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://polygonscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Unichain** | 10 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://uniscan.xyz/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Linea** | 11 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://lineascan.build/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Codex** | 12 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.codex.xyz/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Sonic** | 13 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://sonicscan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **World Chain** | 14 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://worldscan.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Monad** | 15 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://monadvision.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Sei** | 16 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://seiscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **XDC** | 18 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://xdcscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **HyperEVM** | 19 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://hyperscan.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Ink** | 21 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.inkonchain.com/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Plume** | 22 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.plume.org/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **EDGE** | 28 | [`0x5b61381Fc9e58E70EfC13a4A97516997019198ee`](https://pro.edgex.exchange/en-US/explorer/address/0x5b61381Fc9e58E70EfC13a4A97516997019198ee) |
| **Injective** | 29 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://blockscout.injective.network/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Morph** | 30 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://explorer.morph.network/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
| **Pharos** | 31 | [`0x81D40F21F12A8F0E3252Bccb954D722d4c464B64`](https://pharos.socialscan.io/address/0x81D40F21F12A8F0E3252Bccb954D722d4c464B64) |
### TokenMinterV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://etherscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Avalanche** | 1 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://snowtrace.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **OP Mainnet** | 2 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://optimistic.etherscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Arbitrum** | 3 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://arbiscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Base** | 6 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://basescan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Polygon PoS** | 7 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://polygonscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Unichain** | 10 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://uniscan.xyz/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Linea** | 11 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://lineascan.build/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Codex** | 12 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.codex.xyz/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Sonic** | 13 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://sonicscan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **World Chain** | 14 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://worldscan.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Monad** | 15 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://monadvision.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Sei** | 16 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://seiscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **XDC** | 18 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://xdcscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **HyperEVM** | 19 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://hyperscan.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Ink** | 21 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.inkonchain.com/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Plume** | 22 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.plume.org/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **EDGE** | 28 | [`0x338Dfd607855BeEc17f33e539Ac2479853cC8384`](https://pro.edgex.exchange/en-US/explorer/address/0x338Dfd607855BeEc17f33e539Ac2479853cC8384) |
| **Injective** | 29 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://blockscout.injective.network/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Morph** | 30 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://explorer.morph.network/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
| **Pharos** | 31 | [`0xfd78EE919681417d192449715b2594ab58f5D002`](https://pharos.socialscan.io/address/0xfd78EE919681417d192449715b2594ab58f5D002) |
### MessageV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| --------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://etherscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Avalanche** | 1 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://snowtrace.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **OP Mainnet** | 2 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://optimistic.etherscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Arbitrum** | 3 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://arbiscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Base** | 6 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://basescan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Polygon PoS** | 7 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://polygonscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Unichain** | 10 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://uniscan.xyz/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Linea** | 11 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://lineascan.build/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Codex** | 12 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.codex.xyz/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Sonic** | 13 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://sonicscan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **World Chain** | 14 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://worldscan.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Monad** | 15 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://monadvision.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Sei** | 16 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://seiscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **XDC** | 18 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://xdcscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **HyperEVM** | 19 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://hyperscan.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Ink** | 21 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.inkonchain.com/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Plume** | 22 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.plume.org/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **EDGE** | 28 | [`0x88ba38dbB2117879E500c11A0772e2B84Be000B3`](https://pro.edgex.exchange/en-US/explorer/address/0x88ba38dbB2117879E500c11A0772e2B84Be000B3) |
| **Injective** | 29 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://blockscout.injective.network/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Morph** | 30 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://explorer.morph.network/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
| **Pharos** | 31 | [`0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78`](https://pharos.socialscan.io/address/0xec546b6B005471ECf012e5aF77FBeC07e0FD8f78) |
## Testnet contract addresses
### TokenMessengerV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum Sepolia** | 0 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.etherscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Avalanche Fuji** | 1 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.snowtrace.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **OP Sepolia** | 2 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia-optimism.etherscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Arbitrum Sepolia** | 3 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.arbiscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Base Sepolia** | 6 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://base-sepolia.blockscout.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Polygon PoS Amoy** | 7 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://amoy.polygonscan.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Unichain Sepolia** | 10 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://unichain-sepolia.blockscout.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Linea Sepolia** | 11 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.lineascan.build/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Codex Testnet** | 12 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer.codex-stg.xyz/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Sonic Testnet** | 13 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://blaze.soniclabs.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **World Chain Sepolia** | 14 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://sepolia.worldscan.org/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Monad Testnet** | 15 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.monadexplorer.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Sei Testnet** | 16 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.seiscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **XDC Apothem** | 18 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.xdcscan.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **HyperEVM Testnet** | 19 | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` |
| **Ink Testnet** | 21 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer-sepolia.inkonchain.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Plume Testnet** | 22 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet-explorer.plume.org/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Arc Testnet** | 26 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.arcscan.app/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **EDGE Testnet** | 28 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://edge-testnet.explorer.alchemy.com/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Injective Testnet** | 29 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://testnet.blockscout.injective.network/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Morph Hoodi Testnet** | 30 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://explorer-hoodi.morph.network/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
| **Pharos Testnet** | 31 | [`0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA`](https://pharos-testnet.socialscan.io/address/0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA) |
### MessageTransmitterV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum Sepolia** | 0 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.etherscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Avalanche Fuji** | 1 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.snowtrace.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **OP Sepolia** | 2 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia-optimism.etherscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Arbitrum Sepolia** | 3 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.arbiscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Base Sepolia** | 6 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://base-sepolia.blockscout.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Polygon PoS Amoy** | 7 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://amoy.polygonscan.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Unichain Sepolia** | 10 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://unichain-sepolia.blockscout.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Linea Sepolia** | 11 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.lineascan.build/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Codex Testnet** | 12 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer.codex-stg.xyz/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Sonic Testnet** | 13 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://blaze.soniclabs.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **World Chain Sepolia** | 14 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://sepolia.worldscan.org/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Monad Testnet** | 15 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.monadexplorer.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Sei Testnet** | 16 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.seiscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **XDC Apothem** | 18 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.xdcscan.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **HyperEVM Testnet** | 19 | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` |
| **Ink Testnet** | 21 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer-sepolia.inkonchain.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Plume Testnet** | 22 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet-explorer.plume.org/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Arc Testnet** | 26 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.arcscan.app/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **EDGE Testnet** | 28 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://edge-testnet.explorer.alchemy.com/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Injective Testnet** | 29 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://testnet.blockscout.injective.network/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Morph Hoodi Testnet** | 30 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://explorer-hoodi.morph.network/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
| **Pharos Testnet** | 31 | [`0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275`](https://pharos-testnet.socialscan.io/address/0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275) |
### TokenMinterV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum Sepolia** | 0 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.etherscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Avalanche Fuji** | 1 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.snowtrace.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **OP Sepolia** | 2 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia-optimism.etherscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Arbitrum Sepolia** | 3 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.arbiscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Base Sepolia** | 6 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://base-sepolia.blockscout.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Polygon PoS Amoy** | 7 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://amoy.polygonscan.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Unichain Sepolia** | 10 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://unichain-sepolia.blockscout.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Linea Sepolia** | 11 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.lineascan.build/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Codex Testnet** | 12 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer.codex-stg.xyz/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Sonic Testnet** | 13 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://blaze.soniclabs.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **World Chain Sepolia** | 14 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://sepolia.worldscan.org/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Monad Testnet** | 15 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.monadexplorer.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Sei Testnet** | 16 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.seiscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **XDC Apothem** | 18 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.xdcscan.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **HyperEVM Testnet** | 19 | `0xb43db544E2c27092c107639Ad201b3dEfAbcF192` |
| **Ink Testnet** | 21 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer-sepolia.inkonchain.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Plume Testnet** | 22 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet-explorer.plume.org/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Arc Testnet** | 26 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.arcscan.app/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **EDGE Testnet** | 28 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://edge-testnet.explorer.alchemy.com/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Injective Testnet** | 29 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://testnet.blockscout.injective.network/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Morph Hoodi Testnet** | 30 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://explorer-hoodi.morph.network/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
| **Pharos Testnet** | 31 | [`0xb43db544E2c27092c107639Ad201b3dEfAbcF192`](https://pharos-testnet.socialscan.io/address/0xb43db544E2c27092c107639Ad201b3dEfAbcF192) |
### MessageV2
| Blockchain | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| ----------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum Sepolia** | 0 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.etherscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Avalanche Fuji** | 1 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.snowtrace.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **OP Sepolia** | 2 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia-optimism.etherscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Arbitrum Sepolia** | 3 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.arbiscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Base Sepolia** | 6 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://base-sepolia.blockscout.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Polygon PoS Amoy** | 7 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://amoy.polygonscan.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Unichain Sepolia** | 10 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://unichain-sepolia.blockscout.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Linea Sepolia** | 11 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.lineascan.build/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Codex Testnet** | 12 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer.codex-stg.xyz/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Sonic Testnet** | 13 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://blaze.soniclabs.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **World Chain Sepolia** | 14 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://sepolia.worldscan.org/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Monad Testnet** | 15 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.monadexplorer.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Sei Testnet** | 16 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.seiscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **XDC Apothem** | 18 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.xdcscan.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **HyperEVM Testnet** | 19 | `0xbaC0179bB358A8936169a63408C8481D582390C4` |
| **Ink Testnet** | 21 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer-sepolia.inkonchain.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Plume Testnet** | 22 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet-explorer.plume.org/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Arc Testnet** | 26 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.arcscan.app/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **EDGE Testnet** | 28 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://edge-testnet.explorer.alchemy.com/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Injective Testnet** | 29 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://testnet.blockscout.injective.network/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Morph Hoodi Testnet** | 30 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://explorer-hoodi.morph.network/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
| **Pharos Testnet** | 31 | [`0xbaC0179bB358A8936169a63408C8481D582390C4`](https://pharos-testnet.socialscan.io/address/0xbaC0179bB358A8936169a63408C8481D582390C4) |
# EVM Contract Interfaces
Source: https://developers.circle.com/cctp/references/contract-interfaces
Public methods and events for CCTP smart contracts on EVM-compatible blockchains
This page documents the public methods and events exposed by CCTP smart
contracts on EVM-compatible blockchains.
## Contract responsibilities
* **TokenMessengerV2**: Entrypoint for crosschain USDC transfer. Routes messages
to burn USDC on a source blockchain and mint USDC on a destination blockchain.
* **MessageTransmitterV2**: Generic message passing. Sends all messages on the
source blockchain and receives all messages on the destination blockchain.
* **TokenMinterV2**: Responsible for minting and burning USDC. Contains
blockchain-specific settings used by burners and minters.
* **MessageV2**: Provides helper functions for crosschain transfers, such as
`bytes32ToAddress` and `addressToBytes32`, which are commonly used when
bridging between EVM and non-EVM blockchains.
**Gas optimization tip:** If you're writing your own integration, it's more
gas-efficient to
[include address conversion logic directly in your contract](https://github.com/circlefin/evm-cctp-contracts/blob/5f1901a9791b18204e8556bb53fb0dfcb05a832a/src/messages/Message.sol#L146)
rather than calling an external contract.
Full contract source code is
[available on GitHub](https://github.com/circlefin/evm-cctp-contracts).
## TokenMessengerV2
### depositForBurn
Deposits and burns tokens from sender to be minted on destination domain. Minted
tokens will be transferred to `mintRecipient`.
**Note:** There is a \$10 million limit on the amount of USDC that can be burned
in a single CCTP transaction. If the amount exceeds this limit, the transaction
will revert. If you need to transfer more than this limit, break up your
transfers into multiple transactions.
For Fast Transfers, you should always
[check the remaining allowance](/api-reference/cctp/all/get-fast-burn-usdc-allowance)
before initiating a transfer to ensure there is enough to complete your
transfer.
**Parameters**
| Field | Type | Description |
| ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | `uint256` | Amount of tokens to deposit and burn |
| `destinationDomain` | `uint32` | Destination [domain ID](/cctp/concepts/supported-chains-and-domains#domain-identifiers) to send the message to |
| `mintRecipient` | `bytes32` | Address of mint recipient on destination domain (must be converted to 32 byte array, that is, prefix with zeros if needed) |
| `burnToken` | `address` | Address of contract to burn deposited tokens on local domain |
| `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` |
| `maxFee` | `uint256` | Maximum [fee](/cctp/concepts/fees) paid for transfer, specified in units of `burnToken` |
| `minFinalityThreshold` | `uint32` | Minimum [finality threshold](/cctp/concepts/finality-and-block-confirmations) at which burn will be attested |
**Example**
```solidity Solidity theme={null}
// Burn 100 USDC on Ethereum for minting on Avalanche
uint256 amount = 100 * 10**6; // 100 USDC
uint32 destinationDomain = 1; // Avalanche
bytes32 mintRecipient = bytes32(uint256(uint160(recipientAddress)));
address burnToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC on Ethereum
bytes32 destinationCaller = bytes32(0); // Anyone can call receiveMessage
uint256 maxFee = 1000; // 0.001 USDC max fee
uint32 minFinalityThreshold = 1000; // Fast Transfer
tokenMessenger.depositForBurn(
amount,
destinationDomain,
mintRecipient,
burnToken,
destinationCaller,
maxFee,
minFinalityThreshold
);
```
### depositForBurnWithHook
Deposits and burns tokens from sender to be minted on destination domain, and
emits a crosschain message with additional hook data appended. In addition to
the standard `depositForBurn` parameters, `depositForBurnWithHook` accepts a
dynamic-length `hookData` parameter, allowing you to include additional metadata
that can trigger custom logic on the destination blockchain.
**Note:** There is a \$10 million limit on the amount of USDC that can be burned
in a single CCTP transaction. If the amount exceeds this limit, the transaction
will revert. If you need to transfer more than this limit, break up your
transfers into multiple transactions.
For Fast Transfers, you should always
[check the remaining allowance](/api-reference/cctp/all/get-fast-burn-usdc-allowance)
before initiating a transfer to ensure there is enough to complete your
transfer.
**Parameters**
| Field | Type | Description |
| ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | `uint256` | Amount of tokens to burn |
| `destinationDomain` | `uint32` | Destination domain to send the message to |
| `mintRecipient` | `bytes32` | Address of mint recipient on destination domain (must be converted to 32 byte array, that is, prefix with zeros if needed) |
| `burnToken` | `address` | Address of contract to burn deposited tokens on local domain |
| `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` |
| `maxFee` | `uint256` | Maximum fee paid for transfer, specified in units of `burnToken` |
| `minFinalityThreshold` | `uint32` | Minimum finality threshold at which burn will be attested |
| `hookData` | `bytes` | Additional metadata attached to the attested message, used to trigger custom logic on the destination blockchain |
### getMinFeeAmount
Calculates and returns the minimum fee required for a given amount in a Standard
Transfer. If the minimum fee (per unit of `burnToken`) is non-zero, the
specified `maxFee` must be at least the returned minimum fee. Otherwise, the
burn will revert onchain.
**Parameters**
| Field | Type | Description |
| -------- | --------- | ----------------------------------------------------------------------------------------------- |
| `amount` | `uint256` | The amount used to compute the minimum fee. Must be greater than `1` if standard fee is applied |
### handleReceiveFinalizedMessage
Handles incoming message received by the local MessageTransmitter. For a burn
message, mints the associated token to the requested recipient on the local
domain. Validates the function sender is the local MessageTransmitter, and the
remote sender is a registered remote TokenMessenger for `remoteDomain`.
This method is called for messages where `finalityThresholdExecuted` ≥ 2000
(Standard Transfer).
**Parameters**
| Field | Type | Description |
| --------------------------- | ------------------------ | -------------------------------------------------------------- |
| `remoteDomain` | `uint32` | The domain where the message originated from |
| `sender` | `bytes32` | The sender of the message (remote TokenMessenger) |
| `finalityThresholdExecuted` | `uint32` | Specifies the level of finality Circle signed the message with |
| `messageBody` | `bytes` (dynamic length) | The message body bytes |
### handleReceiveUnfinalizedMessage
Handles incoming message received by the local MessageTransmitter. For a burn
message, mints the associated token to the requested recipient on the local
domain. Similar to `handleReceiveFinalizedMessage`, but is called for messages
which are not finalized (`finalityThresholdExecuted` \< 2000) such as Fast
Transfers.
Unlike `handleReceiveFinalizedMessage`, `handleReceiveUnfinalizedMessage`
processes messages with:
* **`expirationBlock`**: If `expirationBlock` ≤ `blockNumber` on the destination
domain, the message will revert and must be re-signed without the expiration
block.
* **`feeExecuted`**: If nonzero, the `feeExecuted` amount is minted to the
`feeRecipient`.
**Parameters**
| Field | Type | Description |
| --------------------------- | ------------------------ | -------------------------------------------------------------------------------------------- |
| `remoteDomain` | `uint32` | The domain where the message originated from |
| `sender` | `bytes32` | The sender of the message (remote TokenMessenger) |
| `finalityThresholdExecuted` | `uint32` | Specifies the level of finality Circle signed the message with |
| `messageBody` | `bytes` (dynamic length) | The message body bytes (see [Message format](/cctp/references/technical-guide#message-body)) |
## MessageTransmitterV2
### `receiveMessage`
Receives message on destination blockchain by passing message and attestation.
Emits `MessageReceived` event. Messages with a given nonce can only be broadcast
successfully once for a pair of domains. The message body of a valid message is
passed to the specified recipient for further processing.
**Parameters**
| Field | Type | Description |
| ------------- | ------- | ------------------------------------------------------------------------------------- |
| `message` | `bytes` | Encoded message (see [Message format](/cctp/references/technical-guide#message-body)) |
| `attestation` | `bytes` | Signed attestation received from Circle's attestation service |
**Example**
```solidity Solidity theme={null}
// Mint USDC on destination chain
bytes memory message = attestationData.message;
bytes memory attestation = attestationData.attestation;
messageTransmitter.receiveMessage(message, attestation);
```
### `sendMessage`
Sends a message to the recipient on the destination domain. Emits a
`MessageSent` event which will be attested by Circle's attestation service.
**Parameters**
| Field | Type | Description |
| ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `destinationDomain` | `uint32` | Destination domain ID to send the message to |
| `recipient` | `bytes32` | Address of recipient on destination domain |
| `destinationCaller` | `bytes32` | Address as `bytes32` which can call `receiveMessage` on destination domain. If set to `bytes32(0)`, any address can call `receiveMessage` |
| `minFinalityThreshold` | `uint32` | Minimum finality threshold requested. A value greater than 2000 is interpreted as 2000 (finalized). Thresholds: 1000 for Fast Transfer (confirmed), 2000 for Standard Transfer (finalized) |
| `messageBody` | `bytes` | application-specific message to be handled by recipient |
## Events
### DepositForBurn
Emitted when USDC is burned on the source blockchain.
**Parameters**
| Field | Type | Indexed | Description |
| --------------------------- | --------- | ------- | ----------------------------------------------------------- |
| `nonce` | `uint64` | Yes | Unique message identifier |
| `burnToken` | `address` | Yes | Address of token burned |
| `amount` | `uint256` | No | Burn amount |
| `depositor` | `address` | Yes | Address of depositor |
| `mintRecipient` | `bytes32` | No | Mint recipient address on destination domain |
| `destinationDomain` | `uint32` | No | Destination domain identifier |
| `destinationTokenMessenger` | `bytes32` | No | Address of TokenMessenger contract on destination domain |
| `destinationCaller` | `bytes32` | No | Authorized caller of `receiveMessage` on destination domain |
| `maxFee` | `uint256` | No | Maximum fee for the transfer |
| `minFinalityThreshold` | `uint32` | No | Minimum finality threshold at which burn will be attested |
### MessageSent
Emitted when a message is sent from the source blockchain.
**Parameters**
| Field | Type | Indexed | Description |
| --------- | ------- | ------- | -------------------- |
| `message` | `bytes` | No | Raw bytes of message |
### MessageReceived
Emitted when a message is received on the destination blockchain.
**Parameters**
| Field | Type | Indexed | Description |
| -------------- | --------- | ------- | ------------------------------------------ |
| `caller` | `address` | Yes | Address that called `receiveMessage` |
| `sourceDomain` | `uint32` | Yes | Source domain identifier |
| `nonce` | `uint64` | Yes | Unique message identifier |
| `sender` | `bytes32` | No | Address of message sender on source domain |
| `messageBody` | `bytes` | No | Message body |
### MintAndWithdraw
Emitted when USDC is minted on the destination blockchain.
**Parameters**
| Field | Type | Indexed | Description |
| --------------- | --------- | ------- | ----------------------------- |
| `mintRecipient` | `address` | Yes | Address receiving minted USDC |
| `amount` | `uint256` | No | Amount minted |
| `mintToken` | `address` | Yes | Address of token minted |
# CoreDepositWallet Contract Interface
Source: https://developers.circle.com/cctp/references/coredepositwallet-contract-interface
The `CoreDepositWallet` contract on HyperEVM allows you to deposit USDC from
HyperEVM to HyperCore. This topic describes the contract interface and the
available deposit functions.
To move USDC from HyperEVM to HyperCore, always call one of the
`CoreDepositWallet` deposit functions (`deposit`, `depositFor`, or
`depositWithAuth`). Only USDC is supported.
Sending USDC or any tokens directly to the `CoreDepositWallet` contract address
doesn't trigger a deposit on HyperCore. The funds are permanently stuck.
## Deposit functions
The `CoreDepositWallet` provides three entry points for depositing USDC from
HyperEVM into HyperCore. All deposits credit a user's balance on HyperCore, on
either the perps or spot DEX.
### `deposit` function
The `deposit` function transfers USDC from the caller's address and credits the
same address on HyperCore, after the caller has approved the `CoreDepositWallet`
to spend their tokens.
**Signature:**
```solidity theme={null}
deposit(uint256 amount, uint32 destinationDex);
```
**Parameters:**
| Parameter | Value |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `amount` | The USDC amount being deposited from HyperEVM to HyperCore |
| `destinationDex` | The HyperCore destination `dex` index. Accepted values are: - `0` → default perps DEX - `type(uint32).max` → spot DEX |
**Token pull:** Uses `transferFrom(msg.sender, address(this), amount)` →
requires prior ERC-20 approve from the `msg.sender` to the core deposit wallet.
**Who is credited:** The `msg.sender` of the transaction on HyperEVM is credited
on HyperCore.
**Examples:**
ERC-20 Approval:
```shell theme={null}
# approve the CoreDepositWallet to spend 100 USDC from the sender
cast send "approve(address,uint256)" 100000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
Depositing to the perps DEX:
```shell theme={null}
# Deposit 100 USDC to the perps DEX (destinationDex = 0)
cast send "deposit(uint256,uint32)" 100000000 0 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
Depositing to the spot DEX:
```shell theme={null}
# Deposit 100 USDC to the spot DEX (destinationDex = uint32.max)
cast send "deposit(uint256,uint32)" 100000000 4294967295 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
**Note:** If the destination DEX value is not supported (spot or perps), the
deposit is credited to the sender's spot balance.
### `depositFor` function
The `depositFor` function transfers USDC from the caller but credits a specified
recipient address on HyperCore. This allows deposits on behalf of another user.
**Signature:**
```solidity theme={null}
depositFor(address recipient, uint256 amount, uint32 destinationId);
```
**Parameters:**
| Parameter | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `recipient` | The recipient address on HyperCore |
| `amount` | The USDC amount being deposited from HyperEVM to HyperCore |
| `destinationId` | The HyperCore destination `dex` index. Accepted values are: - `0` → default perps DEX - `type(uint32).max` → spot DEX |
**Token pull:** Uses `transferFrom(msg.sender, address(this), amount)` →
requires prior ERC-20 approve from the `msg.sender` to the core deposit wallet.
**Who is credited:** The recipient address passed to the function is credited on
HyperCore.
**Examples:**
ERC-20 Approval:
```shell theme={null}
# approve the CoreDepositWallet to spend 100 USDC from the sender
cast send "approve(address,uint256)" 100000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
Depositing to the perps DEX:
```shell theme={null}
# Deposit 100 USDC to the perps DEX (destinationDex = 0)
cast send "depositFor(address,uint256,uint32)" 100000000 0 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
Depositing to the spot DEX:
```shell theme={null}
# Deposit 100 USDC to the spot DEX (destinationDex = uint32.max)
cast send "depositFor(address,uint256,uint32)" 100000000 4294967295 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
**Note:** If the destination DEX value is not supported (spot or perps), the
deposit is credited to the recipient's spot balance.
### `depositWithAuth` function
The `depositWithAuth` function allows depositing USDC using a pre-signed
ERC-3009 authorization. This enables a deposit where the token transfer is
authorized offchain and executed onchain without requiring a prior approve call.
**Signature:**
```solidity theme={null}
depositWithAuth(uint256 amount, uint256 authValidAfter, uint256 authValidBefore, bytes32 authNonce, uint8 v, bytes32 r, bytes32 s, uint32 destinationDex);
```
**Parameters:**
* `amount`: The USDC amount being deposited from HyperEVM to HyperCore
* `authValidAfter`, `authValidBefore`, `authNonce`, `v`, `r`, `s`:
EIP-3009-style authorization fields for `receiveWithAuthorization`
* `destinationDex`: The HyperCore destination `dex` index. Accepted values are:
* `0` → default perps DEX
* `type(uint32).max` → spot DEX
**Token pull:** Calls `token.receiveWithAuthorization(...)`, no prior approve
needed.
**Who is credited:** The `msg.sender` which has to match the `from` address from
the `receiveWithAuthorization` is credited on HyperCore.
**receiveWithAuthorization details:**
* **ERC:** ERC-3009
* **Function Signature:** `ReceiveWithAuthorization`
* **Parameters:**
| Parameter | Value |
| ------------- | -------------------------------------------------------------------------------------------------- |
| `from` | The payer's address (`authorizer`) has to match the `msg.sender` of the `depositWithAuth` function |
| `to` | The `CoreDepositWallet` address (payee) |
| `value` | The auth amount |
| `validAfter` | The time after which this is valid (Unix time) |
| `validBefore` | The time before which this is valid (Unix time) |
| `nonce` | Unique nonce |
| `v` | v of the signature |
| `r` | r of the signature |
| `s` | s of the signature |
**Example:**
The example below illustrates how to generate an ERC-3009 authorization:
```javascript theme={null}
#!/usr/bin/env node
const ethers = require("ethers");
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const wallet = new ethers.Wallet(PRIVATE_KEY);
const provider = new ethers.JsonRpcProvider(
process.env.RPC_URL || "https://rpc.hyperliquid-testnet.xyz/evm",
);
const usdcAddress = "";
const coreDepositWallet = "";
const EIP712_PREFIX = "0x1901";
const amount = ethers.parseUnits("100", 6); // 100 USDC
const nonce = ethers.hexlify(ethers.randomBytes(32));
const validAfter = 0;
const validBefore = Math.floor(Date.now() / 1000) + 3600; // valid for 1 hour
// Minimal ABI for DOMAIN_SEPARATOR
const USDC_ABI = [
{
inputs: [],
name: "DOMAIN_SEPARATOR",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
];
async function getDomainSeparator(usdc) {
try {
return await usdc.DOMAIN_SEPARATOR();
} catch {
const domain = {
name: "USD Coin",
version: "2",
chainId: await provider.getNetwork().then((n) => n.chainId),
verifyingContract: await usdc.getAddress(),
};
return ethers.TypedDataEncoder.hashDomain(domain);
}
}
async function main() {
const usdc = new ethers.Contract(usdcAddress, USDC_ABI, provider);
const domainSeparator = await getDomainSeparator(usdc);
const structHash = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
[
"bytes32",
"address",
"address",
"uint256",
"uint256",
"uint256",
"bytes32",
],
[
ethers.keccak256(
ethers.toUtf8Bytes(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)",
),
),
wallet.address,
coreDepositWallet,
amount,
validAfter,
validBefore,
nonce,
],
),
);
const digest = ethers.keccak256(
ethers.concat([EIP712_PREFIX, domainSeparator, structHash]),
);
const signer = new ethers.SigningKey(PRIVATE_KEY);
const sig = signer.sign(digest);
console.log("Authorization parameters:");
console.log({
amount: amount.toString(),
validAfter,
validBefore,
nonce,
v: sig.v,
r: sig.r,
s: sig.s,
});
}
main().catch(console.error);
```
The example below illustrates how to call the `depositWithAuth` function with
the authorization data:
```shell theme={null}
cast send \
"depositWithAuth(uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32,uint32)" \
0 1735660000 0x 0x 0x \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL
```
**Note:** If the destination DEX value is not supported (spot or `perp`), the
deposit is credited to the `authorizer's` spot balance.
# HyperCore CCTP-Enablement Contract Addresses
Source: https://developers.circle.com/cctp/references/hypercore-contract-addresses
CCTP has additional contracts beyond the standard protocol to enable transfers
to HyperCore. The following sections describe the functions and addresses of
these contracts.
* **`CctpExtension`**: (Arbitrum only) Responsible for transferring USDC from
Arbitrum to HyperCore.
* **`CctpForwarder`**: (HyperEVM only) Responsible for forwarding USDC from
HyperEVM to HyperCore in a CCTP transfer from any non-HyperEVM domain.
* **`CoreDepositWallet`**: (HyperEVM only) Responsible for
[depositing USDC into HyperCore](/cctp/references/coredepositwallet-contract-interface).
This page contains the contract addresses for the HyperCore CCTP-enablement
contracts. For the contract addresses for core CCTP contracts, see
[EVM Contracts and Interfaces](/cctp/evm-smart-contracts),
[Solana Contracts and Interfaces](/cctp/solana-programs), and
[Starknet Contracts and Interfaces](/cctp/starknet-contracts).
## Mainnet contract addresses
### CctpExtension: Mainnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| ------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Arbitrum** | 3 | [`0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE`](https://arbiscan.io/address/0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE) |
### CctpForwarder: Mainnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| ------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **HyperEVM** | 19 | [`0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757`](https://hyperevmscan.io/address/0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757) |
### CoreDepositWallet: Mainnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| ------------ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **HyperEVM** | 19 | [`0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24`](https://hyperevmscan.io/address/0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24) |
## Testnet contract addresses
### CctpExtension: Testnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| -------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **Arbitrum Sepolia** | 3 | [`0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D`](https://sepolia.arbiscan.io/address/0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D) |
### CctpForwarder: Testnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| -------------------- | ----------------------------------------------------------------- | -------------------------------------------- |
| **HyperEVM Testnet** | 19 | `0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2` |
### CoreDepositWallet: Testnet
| Chain | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| -------------------- | ----------------------------------------------------------------- | -------------------------------------------- |
| **HyperEVM Testnet** | 19 | `0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206` |
# CCTP Solana Programs and Interfaces
Source: https://developers.circle.com/cctp/references/solana-programs
Programs for CCTP support on the Solana blockchain
## Overview
Solana CCTP programs are written in Rust and leverage the Anchor framework. The
Solana CCTP protocol implementation is split into two programs:
`MessageTransmitterV2` and `TokenMessengerMinterV2`. `TokenMessengerMinterV2`
encapsulates the capabilities of both `TokenMessengerV2` and `TokenMinterV2`
contracts on EVM chains. To ensure alignment with EVM contracts' logic and
state, and to facilitate upgrades and maintenance, the code and state of Solana
programs reflect the EVM counterparts as closely as possible.
### Mainnet program addresses
| Program | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| :----------------------- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| `MessageTransmitterV2` | 5 | [`CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC`](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC) |
| `TokenMessengerMinterV2` | 5 | [`CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe`](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe) |
### Devnet program addresses
| Program | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| :----------------------- | :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `MessageTransmitterV2` | 5 | [`CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC`](https://solscan.io/account/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC?cluster=devnet) |
| `TokenMessengerMinterV2` | 5 | [`CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe`](https://solscan.io/account/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe?cluster=devnet) |
The Solana CCTP source code is
[available on GitHub](https://github.com/circlefin/solana-cctp-contracts/). The
interface below serves as a reference for permissionless messaging functions
exposed by the programs.
## CCTP interface
The interface below serves as a reference for permissionless messaging functions
exposed by the `TokenMessengerMinter` and `MessageTransmitter` programs. The
full IDLs can be found onchain using a block explorer.
[`MessageTransmitterV2` IDL](https://explorer.solana.com/address/CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe/anchor-program)
and
[`TokenMessengerMinterV2` IDL](https://explorer.solana.com/address/CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC/anchor-program).
*See the instruction rust files or quick-start for PDA information.*
### TokenMessengerMinterV2
#### [`depositForBurn`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/deposit_for_burn.rs)
Deposits and burns tokens from sender to be minted on destination domain. Minted
tokens will be transferred to `mintRecipient`.
**Parameters**
| Field | Type | Description |
| :--------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `amount` | `u64` | Amount of tokens to deposit and burn. |
| `destinationDomain` | `u32` | Destination domain identifier. |
| `mintRecipient` | `Pubkey` | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* |
| `destinationCaller` | `Pubkey` | Address which can call `receiveMessage` on destination domain. If set to `PublicKey.default`, any address can call `receiveMessage` *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* |
| `maxFee` | `u64` | Max fee paid for the transfer, specified in units of the burn token. |
| `minFinalityThreshold` | `u32` | Minimum finality threshold at which burn will be attested |
**Fees**
A fee may be charged for standard USDC transfers. Fees for standard transfers
are set to 0, but are subject to change. See
[CCTP Fees](/cctp/technical-guide#cctp-fees) for more information.
**MessageSent event storage**
To ensure persistent and reliable message storage, MessageSent events are stored
in accounts. MessageSent event accounts are generated client-side, passed into
the instruction call, and assigned to have the `MessageTransmitterV2` program as
the owner. See the
[Quickstart Guide](/cctp/transfer-usdc-on-testnet-from-ethereum-to-avalanche)
for how to generate this account and pass it to the instruction call.
Message nonces are generated offchain, meaning the source messages cannot be
identified from the attestation. Due to this, there is a 5 day window after
sending a message that callers must wait before `reclaim_event_account` can be
called. This is to ensure that the message has been fully processed by Circle's
offchain services.
#### [depositForBurnWithHook](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/deposit_for_burn_with_hook.rs)
Deposits and burns tokens from sender to be minted on destination domain, and
emits a crosschain message with additional hook data appended. In addition to
the standard `deposit_for_burn` parameters, `deposit_for_burn_with_hook` accepts
a dynamic-length `hookData` parameter, allowing the caller to include additional
metadata to the attested message, which can be used to trigger custom logic on
the destination chain.
**Parameters**
| Field | Type | Description |
| :--------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `amount` | `u64` | Amount of tokens to deposit and burn. |
| `destinationDomain` | `u32` | Destination domain identifier. |
| `mintRecipient` | `Pubkey` | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* |
| `destinationCaller` | `Pubkey` | Address which can call `receiveMessage` on destination domain. If set to `PublicKey.default`, any address can call `receiveMessage` *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* |
| `maxFee` | `u64` | Max fee paid for fast burn, specified in units of the burn token. |
| `minFinalityThreshold` | `u32` | Minimum finality threshold at which burn will be attested |
| `hookData` | `Vec` | Additional metadata attached to the attested message, which can be used to trigger custom logic on the destination chain |
#### [handleReceiveFinalizedMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/handle_receive_finalized_message.rs)
Handles incoming message received by the local MessageTransmitter, and takes the
appropriate action. For a burn message, mints the associated token to the
requested recipient on the local domain. Validates the function sender is the
local MessageTransmitter, and the remote sender is a registered remote
TokenMessenger for `remoteDomain`.
Additionally, reads the `feeExecuted` parameter from the BurnMessage. If
nonzero, the `feeExecuted` amount is minted to the `feeRecipient`.
**Parameters**
| Field | Type | Description |
| --------------------------- | -------------------------- | ------------------------------------------------------------ |
| `remoteDomain` | `u32` | The domain where the message originated from |
| `sender` | `Pubkey` | The sender of the message (remote TokenMessenger) |
| `finalityThresholdExecuted` | `u32` | Specifies the level of finality Iris signed the message with |
| `messageBody` | `Vec` (dynamic length) | The message body bytes |
#### [handleReceiveUnfinalizedMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/token-messenger-minter-v2/src/token_messenger_v2/instructions/handle_receive_unfinalized_message.rs)
Handles incoming message received by the local MessageTransmitter, and takes the
appropriate action. For a burn message, mints the associated token to the
requested recipient on the local domain. Validates the function sender is the
local MessageTransmitter, and the remote sender is a registered remote
TokenMessenger for `remoteDomain`.
Similar to `handleReceiveFinalizedMessage`, but is called for messages which are
not finalized (`finalityThresholdExecuted` \< 2000).
Unlike `handleReceiveFinalizedMessage`, `handleReceiveUnfinalizedMessage` has
the following `messageBody` parameter:
* **`expirationBlock`**. If `expirationBlock` ≤ `blockNumber` on the destination
domain, the message will revert and must be re-signed without the expiration
block.
**Parameters**
| Field | Type | Description |
| --------------------------- | -------------------------- | --------------------------------------------------------------------------------- |
| `remoteDomain` | `u32` | The domain where the message originated from |
| `sender` | `Pubkey` | The sender of the message (remote TokenMessenger) |
| `finalityThresholdExecuted` | `u32` | Specifies the level of finality Iris signed the message with |
| `messageBody` | `Vec` (dynamic length) | The message body bytes (see [Message format](/cctp/technical-guide#message-body)) |
### MessageTransmitterV2
#### [`receiveMessage`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/message-transmitter-v2/src/instructions/receive_message.rs)
Messages with a given nonce can only be broadcast successfully once for a pair
of domains. The message body of a valid message is passed to the specified
recipient for further processing.
**Parameters**
| Field | Type | Description |
| :------------ | :-------- | :----------------------------- |
| `message` | `Vec` | Message bytes. |
| `attestation` | `Vec` | Signed attestation of message. |
**Remaining Accounts**
If the `receiveMessage` instruction is being called with a deposit for burn
message that will be received by the `TokenMessengerMinterV2`, additional
`remainingAccounts` are required so they can be passed with the CPI to
`TokenMessengerMinter#handle_receive_finalized_message` or
`TokenMessengerMinter#handle_receive_unfinalized_message`:
| Account Name | PDA Seeds | PDA ProgramId | `isSigner`? | `isWritable`? | Description |
| :------------------------------ | :---------------------------------------------------- | :------------------- | :---------- | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `token_messenger` | `["token_messenger"]` | tokenMessengerMinter | false | false | TokenMessenger Program Account |
| `remote_token_messenger` | `["remote_token_messenger", sourceDomainId]` | tokenMessengerMinter | false | false | Remote token messenger account where the remote token messenger address is stored for the given source domain id |
| `token_minter` | `["token_minter"]` | tokenMessengerMinter | false | true | TokenMinter Program Account |
| `local_token` | `["local_token", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Local token account where the information for the local token (for example, USDCSOL) being minted is stored |
| `token_pair` | `["token_pair", sourceDomainId, sourceTokenInBase58]` | tokenMessengerMinter | false | false | Token pair account where the info for the local and remote tokens are stored. `sourceTokenInBase58` is the remote token that was burned and converted into base58 format. |
| `user_token_account` | N/A | N/A | false | true | User token account that will receive the minted tokens. This address **must** match the `mintRecipient` from the source chain `depositForBurn` call. |
| `custody_token_account` | `["custody", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Custody account that holds the pre-minted USDCSOL that can be minted for CCTP usage. |
| `SPL.token_program_id` | N/A | N/A | false | false | The native SPL token program ID. |
| `token_program_event_authority` | `["__event_authority"]` | tokenMessengerMinter | false | false | Event authority account for the TokenMessengerMinter program. Needed to emit Anchor CPI events. |
| `program` | N/A | N/A | false | false | Program id for the TokenMessengerMinter program. |
#### [`sendMessage`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/v2/message-transmitter-v2/src/instructions/send_message.rs)
Sends a message to the destination domain and recipient. Stores message in a
`MessageSent` account which will be attested by Circle's attestation service.
**Parameters**
| Field | Type | Description |
| :------------------ | :-------- | :---------------------------------------------------- |
| `destinationDomain` | `u32` | Destination domain identifier. |
| `recipient` | `Pubkey` | Address to handle message body on destination domain. |
| `messageBody` | `Vec` | App-specific message to be handled by recipient. |
## Additional Notes
These notes are applicable to all CCTP versions.
### Mint Recipient for Solana as Destination Chain Transfers
When calling `depositForBurn` on a non-Solana chain with Solana as the
destination, the `mintRecipient` should be a **hex encoded USDC token account
address**. The token account\* must exist at the time `receiveMessage` is called
on Solana\* or else this instruction will revert. An example of converting an
address from Base58 to hex taken from the Solana quickstart tutorial in
TypeScript can be seen below:
```typescript TypeScript theme={null}
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { hexlify } from "ethers";
const solanaAddressToHex = (solanaAddress: string): string =>
hexlify(bs58.decode(solanaAddress));
```
### Mint Recipient for Solana as Source Chain Transfers
When specifying the `mintRecipient` for Solana `deposit_for_burn` instruction
calls, the address must be given as the 32 byte version of the hex address in
base58 format. An example taken from the Solana quickstart tutorial in
TypeScript can be seen below:
```typescript TypeScript theme={null}
import { getBytes } from "ethers";
import { PublicKey } from "@solana/web3.js";
const evmAddressToBytes32 = (address: string): string =>
`0x000000000000000000000000${address.replace("0x", "")}`;
const evmAddressToBase58PublicKey = (addressHex: string): PublicKey =>
new PublicKey(getBytes(evmAddressToBytes32(addressHex)));
```
### Program Events
Program events like
[DepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L35-L45)
,
[MintAndWithdraw](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52)
, and
[MessageReceived](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52)
are emitted as Anchor CPI events. This means a self-CPI is made into the program
with the serialized event as instruction data so it is persisted in the
transaction and can be fetched later on as needed. More information can be seen
in the
[Anchor implementation PR](https://github.com/coral-xyz/anchor/pull/2438), and
an example of reading CPI events can be seen in the
[`solana-cctp-contracts` repository](https://github.com/circlefin/solana-cctp-contracts/blob/master/tests/utils.ts#L62-L111).
[MessageSent](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/events.rs#L49-L55)
events are different, as they are stored in accounts. See the
[MessageSent Event Storage section](#depositforburn) for more info.
# CCTP Starknet Contracts and Interfaces
Source: https://developers.circle.com/cctp/references/starknet-contracts
Contracts for CCTP support on the Starknet blockchain
## Overview
Starknet CCTP contracts are written in Cairo and run on a non-EVM zk-rollup.
Transactions executed on Starknet are batched and proven using STARK proofs,
which are then posted to Ethereum L1. This design allows Starknet to inherit
Ethereum's security while offering higher throughput and lower fees.
To align with Starknet's architecture while keeping parity with EVM chains, CCTP
uses two contracts:
* `TokenMessengerMinterV2`: consolidates the responsibilities of
`TokenMessengerV2` (burn + send) and `TokenMinterV2` (receive + mint).
* `MessageTransmitterV2`: provides the messaging layer that emits or receives
attested messages and delivers them to `TokenMessengerMinterV2`.
This mirrors how other non-EVM deployments (for example, Solana) combine
messenger and minter logic while preserving behavior with the EVM equivalents.
## Mainnet contract addresses
| Contract | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| :----------------------- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenMessengerMinterV2` | 25 | [`0x07d421B9cA8aA32DF259965cDA8ACb93F7599F69209A41872AE84638B2A20F2a`](https://voyager.online/contract/0x07d421B9cA8aA32DF259965cDA8ACb93F7599F69209A41872AE84638B2A20F2a) |
| `MessageTransmitterV2` | 25 | [`0x02EBB5777B6dD8B26ea11D68Fdf1D2c85cD2099335328Be845a28c77A8AEf183`](https://voyager.online/contract/0x02EBB5777B6dD8B26ea11D68Fdf1D2c85cD2099335328Be845a28c77A8AEf183) |
## Testnet contract addresses
| Contract | [Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) | Address |
| :----------------------- | :---------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenMessengerMinterV2` | 25 | [`0x04bDdE1E09a4B09a2F95d893D94a967b7717eB85A3f6dEcA8c080Ee01fBc3370`](https://sepolia.voyager.online/contract/0x04bDdE1E09a4B09a2F95d893D94a967b7717eB85A3f6dEcA8c080Ee01fBc3370) |
| `MessageTransmitterV2` | 25 | [`0x04db7926C64f1f32a840F3Fa95cB551f3801a3600Bae87aF87807A54DCE12Fe8`](https://sepolia.voyager.online/contract/0x04db7926C64f1f32a840F3Fa95cB551f3801a3600Bae87aF87807A54DCE12Fe8) |
## CCTP interface
* `TokenMessengerMinterV2`: initiates crosschain burns and mints tokens upon
attested message receipt.
* `MessageTransmitterV2`: emits messages, verifies attestations, and routes
verified messages to the recipient contract.
### TokenMessengerMinterV2 interface
The `TokenMessengerMinterV2` contract consolidates the roles of both
`TokenMessengerV2` and `TokenMinterV2` found on EVM chains. It handles USDC
burns, message emission, and token minting once crosschain messages are attested
by Circle's Iris service.
| Function | Description | Notes |
| :----------------------------------- | :----------------------------------------------------------------------- | :---------------------------------------- |
| `deposit_for_burn` | Burns USDC and emits a crosschain message for minting on another domain. | Standard CCTP transfer initiation. |
| `deposit_for_burn_with_hook` | Same as `deposit_for_burn`, but attaches custom metadata (`hook_data`). | Used for programmable transfers. |
| `handle_receive_finalized_message` | Mints USDC upon receiving a fully finalized message. | Called by `MessageTransmitterV2`. |
| `handle_receive_unfinalized_message` | Processes partially finalized (“Fast Burn”) messages. | Enables faster crosschain transfers. |
| `message_body_version` | Returns supported message format version. | Used for compatibility checks. |
| `local_message_transmitter` | Returns the linked `MessageTransmitterV2` address. | Must match configured domain transmitter. |
### MessageTransmitterV2 interface
The `MessageTransmitterV2` contract provides the core messaging layer for CCTP
on Starknet. It is responsible for emitting, receiving, and validating
crosschain messages, enforcing attestation rules, and ensuring message
uniqueness.
| Function | Description | Notes |
| :-------------------------- | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- |
| `send_message` | Sends a crosschain message with specified domain, recipient, and message body. | Core function for outgoing CCTP messages. |
| `receive_message` | Validates a message and its attestation; delivers message body to the recipient. | Called by an offchain forwarding service with an attestation from Circle to complete the transfer. |
| `get_max_message_body_size` | Returns the maximum allowed message size. | Used by offchain components for validation. |
| `is_nonce_used` | Checks if a message nonce has been processed already. | Prevents message replay. |
| `get_local_domain` | Returns this contract's domain ID. | Expected to be 25 for Starknet. |
| `get_version` | Returns protocol version supported by this transmitter. | Used by Iris attestation service. |
# CCTP on Stellar
Source: https://developers.circle.com/cctp/references/stellar
Learn how to send funds to Stellar addresses and how CCTP handles Stellar USDC.
CCTP on Stellar has two behaviors you must account for when integrating: a
32-byte address format that does not distinguish accounts from contracts, and a
seven-decimal USDC precision that differs from other CCTP supported blockchains.
See [CCTP Stellar Contracts and Interfaces](/cctp/references/stellar-contracts)
for the contract addresses and interfaces.
Always
[use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
`destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
## Stellar address types
On Stellar, addresses are `strkey` strings composed of a type identifier and a
32-byte payload. The type identifier determines the account type: user accounts
(`G`) carry an Ed25519 public key, contracts (`C`) carry a contract ID hash.
Muxed accounts (`M`) are a type of `G` account that additionally embeds a
numeric identifier alongside the Ed25519 public key (see Stellar's
[muxed accounts documentation](https://developers.stellar.org/docs/build/guides/transactions/pooled-accounts-muxed-accounts-memos#muxed-accounts)).
CCTP messages store only the raw 32-byte payload without the type identifier, so
the protocol cannot distinguish between address types and assumes the
`mintRecipient` is always a contract. Use `CctpForwarder` when transferring to
Stellar to ensure funds are forwarded to the intended recipient.
## Use `CctpForwarder` for Stellar recipients
`CctpForwarder` is a publicly callable onchain contract that receives minted
USDC on Stellar and atomically forwards it to `forwardRecipient`. Encode
`forwardRecipient` in hook data as a Stellar `strkey`. The prefix `G`, `M`, or
`C` identifies the recipient address type.
On the source burn, both `mintRecipient` and `destinationCaller` must be set to
the `CctpForwarder` [contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
### How it works
Call `mint_and_forward` on `CctpForwarder` through the Stellar Soroban client
for your language. Pass the raw CCTP message and attestation bytes.
The following shows the onchain contract interface. It is not a TypeScript or
JavaScript function you call directly. Your Soroban client builds an
`invokeHostFunction` operation from these arguments.
```text theme={null}
mint_and_forward(message: Bytes, attestation: Bytes)
```
For TypeScript,
[`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) documents
how to encode arguments and how to simulate, sign, and submit transactions
against [Stellar RPC](https://developers.stellar.org/docs/data/rpc).
Inside `mint_and_forward`, `CctpForwarder` does the following:
1. Validates the message.
2. Extracts `forwardRecipient` from hook data.
3. Calls `receive_message` on `MessageTransmitter`, which mints USDC to
`CctpForwarder`.
4. Transfers the minted USDC to `forwardRecipient`.
5. Runs atomically. Any failure reverts the invocation.
The `CctpForwarder` flow is non-custodial. The mint and the payout to
`forwardRecipient` both run onchain in that single Soroban invocation. Circle
does not take custody of the minted balance in between.
### Hook format
The hook data begins with the reserved magic bytes, followed by versioning and
payload fields. On Stellar, bytes 28 onward carry the length of
`forwardRecipient`, the `forwardRecipient` `strkey`, and any optional trailing
bytes for integrator use.
| Bytes | Type | Data |
| -------------- | --------- | --------------------------------------------------- |
| 0-23 | `bytes24` | Magic. Circle-reserved bytes; use all zero bytes |
| 24-27 | `uint32` | Version; set to `0` |
| 28-31 | `uint32` | `L`: length of `forwardRecipient` in bytes |
| `32..(32+L-1)` | `bytes` | `forwardRecipient` as a `strkey` |
| `(32+L)..` | `bytes` | Optional integrator-defined payload; omit if unused |
#### Building forwarder hook data (example)
The following helper functions validate Stellar contract `strkey` inputs and
build the `hookData` payload for an EVM `depositForBurnWithHook` call:
```ts TypeScript theme={null}
import { StrKey } from "@stellar/stellar-sdk";
/**
* Validates that the input is a Stellar contract address (C…) and decodes it
* to a 0x-prefixed bytes32 hex string suitable for EVM contract calls.
*
* @param strkey - Stellar contract address (C…)
* @returns 0x-prefixed 64-character hex string
* @throws If the input is not a valid contract address
*/
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
/**
* Builds the hookData buffer for a CCTP Forwarder burn message.
*
* Hook data layout:
* bytes 0–23: reserved (zeroed)
* bytes 24–27: hook data version (u32 BE, currently 0)
* bytes 28–31: forward_recipient byte length (u32 BE)
* bytes 32+ : forward_recipient (UTF-8 encoded Stellar strkey)
*
* @param forwardRecipientStrkey - Stellar strkey of the final token recipient (C…, G…, or M…)
* @returns Hook data as a 0x-prefixed hex string
*/
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24); // hook version = 0
hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
return `0x${hookData.toString("hex")}`;
}
interface DepositForBurnWithHookParams {
amount: bigint;
destinationDomain: number;
mintRecipient: `0x${string}`;
burnToken: `0x${string}`;
destinationCaller: `0x${string}`;
maxFee: bigint;
minFinalityThreshold: number;
hookData: `0x${string}`;
}
/**
* Prepares all arguments for an EVM `depositForBurnWithHook` call targeting
* Stellar via the CCTP Forwarder. Converts Stellar strkeys to 0x-prefixed
* bytes32 hex strings and encodes the hook data.
*
* @param amount - Token amount to burn (in EVM token decimals)
* @param cctpForwarderStrkey - Stellar strkey of the CCTP Forwarder contract (C…), used as mintRecipient and destinationCaller
* @param burnToken - EVM address of the token to burn
* @param maxFee - Maximum fee for the burn
* @param minFinalityThreshold - Minimum finality threshold (1000 = fast, 2000 = standard)
* @param forwardRecipientStrkey - Stellar strkey of the final token recipient (C…, G…, or M…), encoded in hookData
*/
function prepareEvmDepositForBurnWithHookToStellar(
amount: bigint,
cctpForwarderStrkey: string,
burnToken: `0x${string}`,
maxFee: bigint,
minFinalityThreshold: number,
forwardRecipientStrkey: string,
): DepositForBurnWithHookParams {
const cctpForwarderHex = contractStrkeyToBytes32(cctpForwarderStrkey);
const hookData = buildCctpForwarderHookData(forwardRecipientStrkey);
return {
amount,
destinationDomain: 27,
mintRecipient: cctpForwarderHex,
burnToken,
destinationCaller: cctpForwarderHex,
maxFee,
minFinalityThreshold,
hookData,
};
}
```
## Stellar addresses in CCTP messages and API responses
[Stellar addresses](#stellar-address-types) are `strkey` strings. CCTP message
fields store only 32-byte address payloads. They omit the `strkey` encoding,
including the `G`, `M`, or `C` type marker, so the raw bytes in the message do
not say whether the address is an account or a contract. `mintRecipient` is
always assumed to be a contract address. You must
[use `CctpForwarder`](#use-cctpforwarder-for-stellar-recipients) to make
transfers to Stellar.
### CCTP message fields
The following tables describe each address field in the CCTP message, explain
how Stellar uses it during a mint (inbound) or burn (outbound), and indicate
whether you need to design around the address type. For the full message layout,
see the [CCTP Technical Guide](/cctp/references/technical-guide#message-format).
#### Inbound transfers to Stellar destination
| Field | Operation | Must design around address type? |
| ------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `sender` | Validate against the source domain `TokenMessenger` mapping. | No |
| `recipient` | Select the Stellar contract that handles the destination `receive_message`. | No, always a contract (`C`) |
| `destinationCaller` | Restrict who may call `receive_message` (`require_auth` compares bytes). | No, compare raw bytes |
| `burnToken` | Map the burned token identifier to Stellar USDC. | No, known asset contract |
| `messageSender` | Not used operationally on Stellar. | No |
| `mintRecipient` | Mint USDC to this 32-byte destination on Stellar. | Yes, always assumed to be a contract; [use `CctpForwarder` for Stellar recipients](#use-cctpforwarder-for-stellar-recipients) |
#### Outbound transfers from Stellar source
| Field | Operation | Must design around address type? |
| ------------------- | ----------------------------------------------------------------------------------------- | -------------------------------- |
| `sender` | 32 byte address payload of the Stellar `TokenMessengerMinterV2` used to perform the burn. | No |
| `burnToken` | Identify the Stellar USDC contract that is burned. | No |
| `mintRecipient` | Encode the recipient on the destination blockchain. | No |
| `messageSender` | Record caller context (not used operationally on Stellar). | No |
| `destinationCaller` | Encode which address may call receive on the destination blockchain. | No |
| `recipient` | Encode the handler contract on the destination blockchain. | No |
### Null address fields in API responses
When a CCTP message involves Stellar, the
[Get messages](/api-reference/cctp/all/get-messages-v2) endpoint returns all
address fields in `decodedMessage` and `decodedMessageBody` as `null` because
the API cannot distinguish a 32-byte Stellar account from a contract. To read
those addresses, parse the raw hex in the `message` field directly.
The following example shows an Ethereum-to-Stellar transfer response with
typical `null` address fields:
```json JSON theme={null}
{
"messages": [
{
"message": "0x...",
"eventNonce": "0",
"attestation": "0x...",
"cctpVersion": 2,
"status": "complete",
"decodedMessage": {
"sourceDomain": "0",
"destinationDomain": "27",
"nonce": "0x0000000000000000000000000000000000000000000000000000000000000000",
"sender": null,
"recipient": null,
"destinationCaller": null,
"minFinalityThreshold": "1000",
"finalityThresholdExecuted": "2000",
"messageBody": "0x...",
"decodedMessageBody": {
"burnToken": null,
"mintRecipient": null,
"amount": "1000000",
"messageSender": null,
"maxFee": "0",
"feeExecuted": "0",
"expirationBlock": "0",
"hookData": null
}
}
}
]
}
```
## USDC precision for CCTP and Stellar
Stellar represents USDC in seven-decimal subunits while other CCTP-supported
blockchains use six. How CCTP handles that difference depends on whether Stellar
is the source or destination blockchain. Regardless of direction, the `amount`
field in a CCTP message is always in six-decimal subunits.
Stellar wallets and SDKs often display seven fractional digits. Use six-decimal
subunits in `amount` when handling CCTP messages offchain.
### Stellar as the source
When Stellar is the source blockchain, the burn debits only through the sixth
decimal digit of the user's balance. Anything in the seventh decimal place stays
in the user's account.
1. A user bridges **0.1234567 USDC** from Stellar to the destination blockchain.
2. Stellar burns **0.1234560 USDC**.
3. **0.0000007 USDC** stays in the user's Stellar account.
4. The CCTP message `amount` is **123456** (six-decimal subunits).
5. The destination blockchain mints **0.123456 USDC** to the recipient.
### Stellar as the destination
When Stellar is the destination blockchain, the mint converts the six-decimal
message `amount` into seven by scaling the integer by 10 (for example, `123456`
becomes `1234560` seven-decimal subunits).
1. A user bridges **0.123456 USDC** from the source blockchain to Stellar.
2. The CCTP message `amount` is **123456** (six-decimal subunits).
3. Stellar mints **0.1234560 USDC** to the recipient.
# CCTP Stellar Contracts and Interfaces
Source: https://developers.circle.com/cctp/references/stellar-contracts
Contracts for CCTP support on the Stellar network
## Overview
Stellar CCTP contracts run on Soroban, Stellar's smart contracts platform. CCTP
message fields use 32-byte address encodings. CCTP treats `mintRecipient` as a
contract address. If the recipient is a Stellar user or
[muxed](https://developers.stellar.org/docs/build/guides/transactions/pooled-accounts-muxed-accounts-memos#muxed-accounts)
account instead, hook data can carry a `forwardRecipient` `strkey` so the
forwarder can send funds to that address.
To align with Stellar address encoding while keeping parity with EVM and other
non-EVM blockchains, CCTP uses three contracts:
* `TokenMessengerMinter`: consolidates the responsibilities of
`TokenMessengerV2` (burn + send) and `TokenMinterV2` (receive + mint). On
mint, `mintRecipient` is treated as a contract address and hooks supply the
`forwardRecipient` when needed.
* `MessageTransmitter`: provides the messaging layer that emits or receives
attested messages and delivers them to `TokenMessengerMinter` (including
`receive_message` for forwarder flows).
* `CctpForwarder`: receives minted USDC and forwards it to `forwardRecipient` in
hook data.
## Mainnet contract addresses
| Contract | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| :--------------------- | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenMessengerMinter` | 27 | [`CAE2G5Z77UP7GYPYGFOWFGW7C7J6I4YP2AFGSADRKQY62SYUFLPNFTXL`](https://stellar.expert/explorer/public/contract/CAE2G5Z77UP7GYPYGFOWFGW7C7J6I4YP2AFGSADRKQY62SYUFLPNFTXL) |
| `MessageTransmitter` | 27 | [`CACMENFFJPJMSDAJQLX4R7K3SFZIW2LJSE3R2UMLGSWHFHS353FVXAZV`](https://stellar.expert/explorer/public/contract/CACMENFFJPJMSDAJQLX4R7K3SFZIW2LJSE3R2UMLGSWHFHS353FVXAZV) |
| `CctpForwarder` | 27 | [`CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T`](https://stellar.expert/explorer/public/contract/CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T) |
## Testnet contract addresses
| Contract | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address |
| :--------------------- | :----------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenMessengerMinter` | 27 | [`CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP`](https://stellar.expert/explorer/testnet/contract/CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP) |
| `MessageTransmitter` | 27 | [`CBJ6MTCKKZG73PMDZCJMSFRD7DQEMI4FKDH7CGDSV4W6FHCRBCQAVVJY`](https://stellar.expert/explorer/testnet/contract/CBJ6MTCKKZG73PMDZCJMSFRD7DQEMI4FKDH7CGDSV4W6FHCRBCQAVVJY) |
| `CctpForwarder` | 27 | [`CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ`](https://stellar.expert/explorer/testnet/contract/CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ) |
## CCTP interface
* `TokenMessengerMinter`: initiates crosschain burns and mints tokens upon
attested message receipt.
* `MessageTransmitter`: emits messages, verifies attestations, and routes
verified messages to the recipient contract.
* `CctpForwarder`: completes mint and forward in one transaction when hook data
supplies a `forwardRecipient` `strkey`.
### TokenMessengerMinter interface
The `TokenMessengerMinter` contract consolidates the roles of both
`TokenMessengerV2` and `TokenMinterV2` found on EVM chains. It handles USDC
burns, message emission, and token minting once crosschain messages are attested
by Circle's Iris service. On Stellar it assumes `mintRecipient` is a contract.
Account recipients use `CctpForwarder` and hook-qualified `forwardRecipient`
bytes.
| Function | Description | Notes |
| :----------------------------------- | :----------------------------------------------------------------------- | :------------------------------------------------------ |
| `deposit_for_burn` | Burns USDC and emits a crosschain message for minting on another domain. | Standard CCTP transfer initiation. |
| `deposit_for_burn_with_hook` | Same as `deposit_for_burn`, but attaches custom metadata (`hook_data`). | Used for programmable transfers and Stellar forwarding. |
| `handle_receive_finalized_message` | Mints USDC upon receiving a fully finalized message. | Called by `MessageTransmitter`. |
| `handle_receive_unfinalized_message` | Processes partially finalized ("Fast Burn") messages. | Enables faster crosschain transfers. |
| `message_body_version` | Returns supported message format version. | Used for compatibility checks. |
| `local_message_transmitter` | Returns the linked `MessageTransmitter` address. | Must match configured domain transmitter. |
### MessageTransmitter interface
The `MessageTransmitter` contract provides the core messaging layer for CCTP on
Stellar. It is responsible for emitting, receiving, and validating crosschain
messages, enforcing attestation rules, and ensuring message uniqueness.
| Function | Description | Notes |
| :-------------------------- | :------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| `send_message` | Sends a crosschain message with specified domain, recipient, and message body. | Core function for outgoing CCTP messages. |
| `receive_message` | Validates a message and its attestation; delivers message body to the recipient. | Called by an offchain forwarding service (or by `CctpForwarder` in the forwarder flow) with an attestation from Circle. |
| `get_max_message_body_size` | Returns the maximum allowed message size. | Used by offchain components for validation. |
| `is_nonce_used` | Checks if a message nonce has been processed already. | Prevents message replay. |
| `get_local_domain` | Returns this contract's domain ID. | Expected to be 27 for Stellar. |
| `get_version` | Returns protocol version supported by this transmitter. | Used by Iris attestation service. |
### CctpForwarder interface
The `CctpForwarder` contract calls `receive_message` on `MessageTransmitter`,
takes the mint, and transfers USDC to `forwardRecipient` parsed from
`hook_data`. See [Hook format](/cctp/references/stellar#hook-format) for
details.
| Function | Description | Notes |
| :----------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
| `mint_and_forward(message: Bytes, attestation: Bytes)` | Verifies message and attestation. Runs `receive_message` so USDC mints to `CctpForwarder`. Sends USDC to `forwardRecipient` from hooks. | Atomic, any failure reverts. |
The `CctpForwarder` flow is non-custodial. `mint_and_forward` mints to this
contract and pays `forwardRecipient` in one atomic Soroban invocation. Circle
does not take custody of the minted balance in between.
# CCTP Technical Guide
Source: https://developers.circle.com/cctp/references/technical-guide
Technical explainer for CCTP
## Message passing
Cross-Chain Transfer Protocol (CCTP) uses generalized message passing to
facilitate the native burning and minting of USDC across supported blockchains,
also known as
[domains](/cctp/cctp-supported-blockchains#cctp-supported-domains). Message
passing is a three-step process:
1. An onchain component on the source domain emits a message.
2. Circle's offchain attestation service signs the message.
3. The onchain component at the destination domain receives the message, and
forwards the message body to the specified recipient.
Onchain components serve the same purpose across all domains, but their
implementations differ between EVM-compatible and non-EVM domains. Moreover,
there are both implementation and naming differences between CCTP V2 and
previous versions due to the addition of Fast Transfer and other improvements.
### For EVM chains
The relationship between CCTP's onchain components and Circle's offchain
Attestation Service is illustrated below for a burn-and-mint of USDC between
EVM-compatible domains:
On EVM domains, the onchain component for crosschain burning and minting is
called **TokenMessengerV2**, which is built on top of **MessageTransmitterV2**,
an onchain component for generalized message passing.
In the diagram, a token depositor calls the
[TokenMessengerV2#depositForBurn](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/TokenMessengerV2.sol#L158)
function to deposit a native token (such as USDC), which delegates to the
TokenMinterV2 contract to burn the token. The **TokenMessengerV2** contract then
sends a message via the
[MessageTransmitterV2#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L143)
function. After
[sufficient block confirmations](/cctp/required-block-confirmations), Circle's
offchain attestation service, Iris, signs the message. An API consumer must
query this attestation and submits it onchain to the destination domain's
[MessageTransmitterV2#receiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L206)
function.
To send an arbitrary message, directly call
[MessageTransmitterV2#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/v2/MessageTransmitterV2.sol#L143).
The message recipient must implement the following methods to handle messages
based on their finality threshold:
* Implement
[IMessageHandlerV2#handleReceiveFinalizedMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/interfaces/v2/IMessageHandlerV2.sol#L35)
to receive messages with `finalityThresholdExecuted` ≥ 2000.
* Implement
[IMessageHandlerV2#handleReceiveUnfinalizedMessage](https://github.com/circlefin/evm-cctp-contracts/blob/63ab1f0ac06ce0793c0bbfbb8d09816bc211386d/src/interfaces/v2/IMessageHandlerV2.sol#L51)
to receive messages with `finalityThresholdExecuted` \< 2000.
This distinction allows the recipient to control the level of finality it
requires before accepting a message.
### For non-EVM chains
CCTP is also available on several non-EVM blockchains where USDC is natively
issued, extending crosschain capabilities to the broader ecosystem.
On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. For inbound transfers, use
[`CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
so funds reach the correct recipient. See
[CCTP on Stellar](/cctp/references/stellar).
## Message format
### Message header
The top-level message header format is standard for all messages passing through
CCTP.
| Field | Offset | Solidity Type | Length (bytes) | Description |
| --------------------------- | ------ | ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `version` | 0 | `uint32` | 4 | Version identifier - use 1 for CCTP |
| `sourceDomain` | 4 | `uint32` | 4 | Source domain ID |
| `destinationDomain` | 8 | `uint32` | 4 | Destination domain ID |
| `nonce` | 12 | `bytes32` | 32 | Unique message nonce (see [CCTP V2 Nonces](#cctp-v2-nonces)) |
| `sender` | 44 | `bytes32` | 32 | Address of MessageTransmitterV2 caller on source domain |
| `recipient` | 76 | `bytes32` | 32 | Address to handle message body on destination domain |
| `destinationCaller` | 108 | `bytes32` | 32 | Address permitted to call MessageTransmitterV2 on destination domain, or bytes32(0) if message can be received by any address |
| `minFinalityThreshold` | 140 | `uint32` | 4 | Minimum finality threshold before allowed to attest (see [CCTP V2 Finality Thresholds](#cctp-v2-finality-thresholds)) |
| `finalityThresholdExecuted` | 144 | `uint32` | 4 | Actual finality threshold executed from source chain (see [CCTP V2 Finality Thresholds](#cctp-v2-finality-thresholds)) |
| `messageBody` | 148 | `bytes` | dynamic | App-specific message to be handled by recipient |
#### Nonces
A CCTP nonce is a unique identifier for a message that can only be used once on
the destination domain. Circle assigns CCTP nonces offchain. The nonce for each
message in a transaction can be queried through the
[`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint, using
the transaction hash as a query parameter.
**Why `bytes32` type for addresses**
CCTP is built to support EVM chains, which use 20 byte addresses, and non-EVM
chains, many of which use 32 byte addresses. Circle provides a
[`Message.sol` library](https://github.com/circlefin/evm-cctp-contracts/blob/40111601620071988e94e39274c8f48d6f406d6d/src/messages/Message.sol#L145-L157)
as a reference implementation for converting between address and `bytes32` in
Solidity.
### Message body
The message format includes a dynamically sized `messageBody` field, used for
application-specific messages. For example, `TokenMessengerV2` defines a
[BurnMessageV2](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol)
with data related to crosschain transfers.
| Field | Offset | Solidity Type | Length (bytes) | Description |
| ----------------- | ------ | ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `version` | 0 | `uint32` | 4 | Version identifier - use 1 for CCTP |
| `burnToken` | 4 | `bytes32` | 32 | Address of burned token on source domain |
| `mintRecipient` | 36 | `bytes32` | 32 | Address to receive minted tokens on destination domain |
| `amount` | 68 | `uint256` | 32 | Amount of burned tokens |
| `messageSender` | 100 | `bytes32` | 32 | Address of caller of `depositForBurn` (or `depositForBurnWithCaller`) on source domain |
| `maxFee` | 132 | `uint256` | 32 | Maximum fee to pay on the destination domain, specified in units of `burnToken` |
| `feeExecuted` | 164 | `uint256` | 32 | Actual fee charged on the destination domain, specified in units of `burnToken` (capped by `maxFee`) |
| `expirationBlock` | 196 | `uint256` | 32 | An expiration block 24 hours in the future is encoded in the message before signing by attestation service, and is respected on the destination chain. If the burn expires, it must be re-signed. Expiration acts as a safety mechanism against problems with finalization, such as a stuck sequencer. |
| `hookData` | 228 | `bytes` | dynamic | Arbitrary data to be included in the `depositForBurn` on source domain and to be executed on destination domain |
**`expirationBlock` on ARB-stack blockchains**
For ARB-stack destination blockchains (Arbitrum, EDGE, and Plume), the
`expirationBlock` is an Ethereum (L1) block number, not the L2 block number.
ARB-stack blockchains track blocks internally using the parent blockchain
(Ethereum). When validating expiration for these blockchains, compare the
`expirationBlock` value against the current Ethereum block number, not the L2
block number.
## API hosts and endpoints
CCTP provides a set of API hosts and endpoints to manage messages, attestations,
and transaction details for your crosschain USDC transfers.
### API service hosts
| Environment | URL |
| :---------- | :------------------------------------ |
| **Testnet** | `https://iris-api-sandbox.circle.com` |
| **Mainnet** | `https://iris-api.circle.com` |
**API Service Rate Limit**
The CCTP API service rate limit is 35 requests per second. If you exceed 35
requests per second, the service blocks all API requests for the next 5 minutes
and returns an HTTP 429 response.
### API endpoints
CCTP endpoints enable advanced capabilities such as fetching attestations for
**Standard Transfer** or **Fast Transfer** burn events, verifying public keys
across versions, accessing transaction details, querying fast transfer
allowances and fees, and initiating re-attestation processes. Below is an
overview of the CCTP public endpoints. Click on any endpoint for its API
reference.
| Endpoint | Description | Use Case |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [`GET /v2/publicKeys`](/api-reference/cctp/all/get-public-keys-v2) | Returns public keys for validating attestations across all supported CCTP versions. | Retrieve public keys to verify attestation authenticity for crosschain transactions. |
| [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) | Retrieves messages and attestations for a given transaction or nonce, supporting messages for all CCTP versions. | Fetch attestation status and transaction details. |
| [`POST /v2/reattest`](/api-reference/cctp/all/reattest-message) | Re-attests a soft finality V2 message to achieve finality or revive expired Fast Transfer burns. | Handle edge cases requiring updated attestations or finalize transactions with stricter rules. |
| [`GET /v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance) | Retrieves the current USDC Fast Transfer allowance remaining. | Monitor available allowance for Fast Transfer burns in real-time. |
| [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) | Returns the fees for USDC transfers between specified source and destination domains. | Calculate transaction costs before initiating a Fast or Standard Transfer. |
**Deprecated endpoint**
The endpoint `/v2/fastBurn/USDC/fees` is deprecated. Use
[`/v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) instead to
retrieve both Fast and Standard Transfer fees.
**Note:** This deprecation does **not** affect
[`/v2/fastBurn/USDC/allowance`](/api-reference/cctp/all/get-fast-burn-usdc-allowance)
(see preceding table), which remains active and valid.
## Finality thresholds
CCTP has the concept of a finality threshold, which is a chain-agnostic
representation of the confirmation level required before an attestation is
issued. This allows integrators to specify how many confirmations are needed
based on their risk tolerance or use case.
In CCTP, each message specifies a `minFinalityThreshold`. This threshold
indicates the minimum level of confirmation required for Circle's attestation
service (Iris) to attest to the message. Iris will not attest to a message at a
confirmation level below the specified minimum threshold. This allows
applications to enforce a desired level of finality before acting on an
attestation on the destination chain.
### Defined finality thresholds
CCTP V2 defines the following finality thresholds:
| Finality Threshold | Value |
| ------------------ | ----- |
| **Confirmed** | 1000 |
| **Finalized** | 2000 |
### Messages and finality
* Messages with a `minFinalityThreshold` of **1000** or lower are considered
**Fast** messages. These messages are eligible for fast attestation at the
*confirmed* level by Iris.
* Messages with a `minFinalityThreshold` of **2000** are considered **Standard**
messages. These messages are attested to at the *finalized* level by Iris.
Only two finality thresholds are supported. Any `minFinalityThreshold` value
below **1000** is treated as **1000**, and any value above **1000** is treated
as **2000**.
## Fees
For information about CCTP transfer fees, including fee tables by blockchain,
the `maxFee` parameter, and Standard Transfer fee switch support, see
[CCTP Fees](/cctp/concepts/fees).
## Hooks
Hooks in CCTP V2 are metadata that can be attached to a burn message, allowing
integrators to execute custom logic at the destination chain. Hook execution is
left entirely to the integrator, offering maximum flexibility and enabling
broader crosschain compatibility without altering the core CCTP protocol.
### Design overview
CCTP does not implement hook execution in the core protocol. Instead, hooks are
treated as opaque metadata passed along with the burn message. This design
allows integrators to define and control how hooks are processed on the
destination chain, based on their own infrastructure and trust model.
### Key benefits
* **Maximum flexibility for integrators**
* Determine execution timing: pre-mint or post-mint
* Implement custom recovery or error-handling strategies if hook execution
fails
* Choose any execution environment (EVM or non-EVM); even non-EVM chains can
support Hooks as data passed into a function call.
* **Improved Compliance and Security Separation**
* **Compliance**: By delegating hook execution to the integrator, the protocol
maintains a clear boundary between CCTP's core message-passing capabilities
and application-specific logic. This modular approach helps integrators meet
their own compliance requirements with greater flexibility.
* **Security**: By keeping hook execution outside the core protocol, CCTP
maintains a smaller and more focused security surface, while allowing
integrators to manage their own execution environments independently.
## Security audit
The CCTP smart contracts have been independently audited by two third-party
security firms:
* CCTP was audited by
[ChainSecurity (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/ChainSecurity_Circle_CCTP_V2_audit%20\(1\).pdf)
and
[OtterSec (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/public_evm_cctp_audit_final%20\(2\).pdf)
* CCTP (with Standard Transfer fee switch) was audited by
[ChainSecurity (PDF)](https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/CCTP/ChainSecurity_Circle_CCTP_audit_2025-07.pdf)
# Cross-Chain Transfer Protocol V1
Source: https://developers.circle.com/cctp/v1
Move USDC securely across blockchains and simplify user experience
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
## Overview
**Cross-Chain Transfer Protocol V1** is a permissionless onchain utility that
facilitates USDC transfers securely between blockchain networks via native
burning and minting. Circle created CCTP to improve capital efficiency and
minimize trust requirements when using USDC across blockchain networks. CCTP V1
enables developers to build multichain applications that allow users to perform
1:1 transfers of USDC securely across blockchains.
**Note:** CCTP V1 only supports **Standard Transfer**, constrained by
blockchain finality on the source blockchain. Later [CCTP](/cctp) versions
support **Fast Transfer** and **Hooks**, in addition to also supporting
**Standard Transfer**.
## Understanding the Problem
Blockchain networks often operate in siloed environments and cannot natively
communicate with one another. While some ecosystems, such as Cosmos, use
built-in protocols like the Inter-Blockchain Communication (IBC) protocol to
enable data transmission between their appchains, direct communication between
isolated networks, such as Ethereum and Avalanche, remains infeasible.
Traditional bridges exist to address this limitation by enabling the transfer of
digital assets, such as USDC, across blockchains. However, these bridges come
with significant drawbacks. Two common methods, lock-and-mint bridging and
liquidity pool bridging, require locking USDC liquidity in third-party smart
contracts. This approach reduces capital efficiency and introduces additional
trust assumptions.
## Design Approach
As a low-level primitive, CCTP V1 can be embedded within any app or wallet -
even existing bridges - to enhance and simplify the user experience for
cross-chain use cases. With USDC circulating across a large number of blockchain
networks, CCTP V1 can connect and unify liquidity across disparate ecosystems
where it's supported.
CCTP V1 is built on generalized message passing and designed for composability,
enabling a wide range of use cases. Developers can extend its functionality
beyond just moving USDC between blockchains. For example, you can create a flow
where USDC is sent across chains and automatically deposited into a DeFi lending
pool after the transfer, allowing it to generate yield in an automated manner.
This experience can be designed to feel like a seamless, single transaction for
the end user.
## How CCTP V1 works
**Standard Transfer** is the default method in CCTP V1 for transferring USDC
across blockchains, which involves burning USDC on the source chain and minting
it on the destination chain. It relies on transaction finality on the source
chain and uses Circle's Attestation Service to enable standard-finality
(hard
finality) transfers. The process includes the following steps:
1. **Initiation**. A user accesses an app powered by CCTP V1 and initiates a
Standard Transfer of USDC, specifying the recipient's wallet address on the
destination chain.
2. **Burn Event**. The app facilitates a burn of the specified USDC amount on
the source blockchain.
3. **Attestation**. Circle's Attestation Service observes the burn event and,
after observing hard finality on the source chain, issues a signed
attestation. Hard finality ensures the burn is irreversible (about 13 to 19
minutes for Ethereum and L2 chains.)
4. **Mint Event**. The app retrieves the signed attestation from Circle and uses
it to mint USDC on the destination chain.
5. **Completion**. The recipient wallet address receives the newly minted USDC
on the destination blockchain, completing the transfer.
**Standard Transfer** prioritizes reliability and security, making it suitable
for scenarios where finality wait times are acceptable.
## Use Cases
CCTP V1 enables developers to build novel cross-chain apps that integrate
functionalities like trading, lending, payments, NFTs, and gaming, while
simplifying the user experience. Below are some practical examples of how you
can leverage CCTP V1 in your applications, either directly or indirectly by
routing USDC behind the scenes:
### Cross-chain rebalancing
Market makers, fillers/solvers, exchanges, and bridges can use CCTP V1 to manage
liquidity more efficiently. By securely rebalancing USDC holdings across
blockchains, you can reduce operational costs, meet demand, and take advantage
of market opportunities with minimal latency.
### Cross-chain swaps
With CCTP V1, users can quickly swap between digital assets on different
blockchains by routing through USDC. Users can also swap for USDC and
automatically trigger subsequent actions on the destination chain, enabling
seamless cross-chain transactions.
### Cross-chain purchases
Automate cross-chain purchases with CCTP V1. For example, a user can use USDC on
one chain to purchase an NFT on a decentralized exchange on another chain and
list it for sale on an NFT marketplace. When the transaction is initiated, CCTP
V1 routes USDC across chains to buy the NFT and opens the listing on the
marketplace—all in one streamlined flow.
### Simplify cross-chain complexities
Simplify the cross-chain experience by using USDC as collateral on one chain to
open a borrowing position on a lending protocol on another chain. With CCTP V1,
USDC can move quickly between blockchains, allowing users to onboard to new
applications without switching wallets or managing multi-chain complexities.
# CCTP Aptos Packages and Interfaces V1
Source: https://developers.circle.com/cctp/v1/aptos-packages
Packages for CCTP V1 support on the Aptos blockchain
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
## Overview
The CCTP V1 Aptos smart contract implementation is written in
[Move](https://aptos.dev/en/build/smart-contracts). The Aptos CCTP V1
implementation is split into two packages: `MessageTransmitter` and
`TokenMessengerMinter`. `TokenMessengerMinter` encapsulates the functionality of
both `TokenMessenger` and `TokenMinter` contracts on EVM chains. To ensure
alignment with EVM contracts logic and state, and to facilitate future upgrades
and maintenance, the code and state of the Aptos packages reflect the EVM
counterparts as closely as possible.
The key difference with Aptos packages from EVM and other CCTP V1
implementations is the receive message flow. Since the Move language uses static
dispatch and requires all dependencies to be available at compile time, the
`MessageTransmitter` `receive_message` function cannot call into the receiver
package (e.g. `TokenMessengerMinter` for USDC transfers). The workaround for
this limitation is that callers of `message_transmitter::receive_message` must
also atomically (in the same transaction or
[move script](https://aptos.dev/en/build/smart-contracts/scripts)) call into the
receiver package's `handle_receive_message` function with a Receipt object
returned from `receive_message`, and then pass a `Receipt` object back into the
`message_transmitter::complete_receive_message` function to complete the message
and destroy the `Receipt` object. Please see the interface and examples below
for more information on this flow.
Below are the known package IDs and object IDs on Aptos testnet and mainnet.
### Testnet
#### Package IDs
| Package | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | 9 | `0x081e86cebf457a0c6004f35bd648a2794698f52e0dde09a48619dcd3d4cc23d9` |
| TokenMessengerMinter | 9 | `0x5f9b937419dda90aa06c1836b7847f65bbbe3f1217567758dc2488be31a477b9` |
#### Object IDs
| Object | Object ID |
| :------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | `0xcbb70e4f5d89b4a37e850c22d7c994e32c31e9cf693e9633784e482e9a879e0c` |
| TokenMessengerMinter | `0x1fbf4458a00a842a4774f441fac7a41f2da0488dd93a43880e76d58789144e17` |
| Stablecoin | `0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832` |
CCTP V1 and Stablcoin use a shared package `AptosExtensions` deployed at
`0xb75a74c6f8fddb93fdc00194e2295d8d5c3f6a721e79a2b86884394dcc554f8f`
### Mainnet
#### Package IDs
| Package | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | 9 | `0x177e17751820e4b4371873ca8c30279be63bdea63b88ed0f2239c2eea10f1772` |
| TokenMessengerMinter | 9 | `0x9bce6734f7b63e835108e3bd8c36743d4709fe435f44791918801d0989640a9d` |
#### Object IDs
| Object | Object ID |
| :------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | `0x45bf7f71e44750f2b2a7a1fea21fc44b4a83ba5d68ab10c7a3935f6d8cbdbc75` |
| TokenMessengerMinter | `0x9e6702a472080ea3caaf6ba9dfaa6effad2290a9ba9adaacd5af5c618e42782d` |
| Stablecoin | `0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b` |
The shared package `AptosExtensions` is deployed at
`0x98bce69c31ee2cf91ac50a3f38db7b422e3df7cdde9fe672ee1d03538a6aeae0`
## Interface
The Aptos CCTP V1 source code is
[available on GitHub](https://github.com/circlefin/aptos-cctp/).
The interface below serves as a reference for permissionless messaging functions
exposed by the programs.
### TokenMessengerMinter
#### [deposit\_for\_burn](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L123)
Burns passed in `FungibleAsset` from sender to be minted on the destination
domain. Minted tokens will be transferred to `mint_recipient` on the destination
chain. The `mint_recipient` can be an account address or a store address. The
`deposit_for_burn` interface and functionality is very similar to the EVM
implementation. The asset parameter is the key difference due to how passing
tokens around on Aptos works. The asset parameter is Aptos Fungible Asset type,
defining information of the token to deposit and burn.
Nonce reserved for the message is returned, but it is not required to do
anything with this return value, they are returned for convenience.
**Parameters**
| Field | Type | Description |
| :------------------ | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer executing the transaction |
| asset | `FungibleAsset` | Asset to be burned. |
| destination\_domain | `u32` | Destination domain identifier. |
| mint\_recipient | `address` | Address of mint recipient on destination domain. Can be an account address or [store address](https://aptos.dev/en/build/smart-contracts/fungible-asset#managing-stores-advanced). *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
#### [deposit\_for\_burn\_with\_caller](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L147)
The same as deposit\_for\_burn, but with an additional parameter:
`destination_caller`. This parameter specifies which address has permission to
call `receiveMessage` on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| :------------------ | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer executing the transaction |
| asset | `FungibleAsset` | Asset to be burned. |
| destination\_domain | `u32` | Destination domain identifier. |
| mint\_recipient | `address` | Address of mint recipient on destination domain. Can be an account address or [store address](https://aptos.dev/en/build/smart-contracts/fungible-asset#managing-stores-advanced). *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
| destination\_caller | `address` | Address of caller on destination chain. |
**Destination Caller Notes**
If the `destination_caller` does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard `deposit_for_burn` should be preferred for
use cases where a specific destination caller is not required.
*Note: If destination is a non-Move chain,* `destination_caller` *address should
be converted to hex and passed in using the @0x123 address format.*
#### [replace\_deposit\_for\_burn\_with](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#L171)
Replace a BurnMessage to change the mint recipient and/or destination caller.
Allows the sender of a previous BurnMessage (created by `deposit_for_burn` or
`deposit_for_burn_with_caller`) to send a new BurnMessage to replace the
original.
**Remarks:**
* Only the sender of the original `deposit_for_burn` transaction has access to
call `replace_deposit_for_burn`
* The new `BurnMessage` will reuse the amount and burn token of the original,
without requiring a new `FA` deposit.
* The resulting mint will supersede the original mint, as long as the original
mint has not confirmed yet onchain.
* A valid attestation is required before calling this function.
* This is useful in situations where the user specified an incorrect address and
has no way to safely mint the previously burned USDC.
**Parameters**
| Field | Type | Description |
| :----------------------- | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer executing the transaction |
| original\_message | `vector` | Original message bytes (to replace). |
| original\_attestation | `vector` | Original attestation |
| new\_destination\_caller | `Option` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. |
| new\_mint\_recipient | `Option` | The new mint recipient, which may be the same as the original mint recipient, or different. |
#### [handle\_receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/sources/token_messenger/token_messenger.move#241)
Handles an incoming message that has already been verified by
`message_transmitter`, and mints USDC to the recipient for valid messages. This
function can only be called with a mutable reference to a `Receipt` object,
which can only be created via a call with a valid message to the
`message_transmitter::receive_message` function. In this function
`MessageTransmitter::complete_receive_message` is called with the `Receipt` to
emit the `MessageReceived` event to complete the message and destroy Receipt.
This should be called in a single transaction after calling
`message_transmitter::receive_message`. EOAs can execute these functions in a
single
[Move script](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/scripts/handle_receive_message.move#L5).
See the Examples section for the entire flow of receiving a message.
**Parameters**
| Field | Type | Description |
| :------ | :-------- | :--------------------------------------------------------------- |
| receipt | `Receipt` | Receipt struct returned by `message_transmitter:receive_message` |
### MessageTransmitter
#### [receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L287)
Receives a message emitted from a source chain. Messages with a given `nonce`
can only be received once for a (sourceDomain, destinationDomain) pair.
This function returns a `Receipt` struct
([Hot Potato](https://medium.com/@borispovod/move-hot-potato-pattern-bbc48a48d93c))
after validating the attestation and marking the nonce as used. In order to
destroy the `Receipt` and complete the message, in a single transaction,
`handle_receive_message()` must be called with the `Receipt` in the receiver
package. EOAs can execute these functions in a single
[Move script](https://github.com/circlefin/aptos-cctp/blob/master/packages/token_messenger_minter/scripts/handle_receive_message.move#L5).
Returns a `Receipt` object that must be handled by the intended receiving
package, and completed via a `complete_receive_message` call before the
transaction ends. See the
[Examples](/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base) section for
more information on receiving USDC transfers.
**Parameters**
| Field | Type | Description |
| :------------- | :----------- | :------------------------------- |
| caller | `Signer` | Signer executing the transaction |
| message\_bytes | `vector` | Message bytes |
| attestation | `vector` | Attestation |
#### [complete\_receive\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L330)
Completes the message by emitting a `MessageReceived` event for a receipt and
destroying the receipt.
**Parameters**
| Field | Type | Description |
| :------ | :-------- | :----------------------------------------- |
| caller | `Signer` | Signer for the `receipt.recipient` address |
| receipt | `Receipt` | Receipt struct to be destroyed |
#### [send\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L169)
Sends a message to the destination domain and recipient. Nonce reserved for the
message is returned, but it is not required to do anything with this struct, it
is returned for convenience.
**Remarks:**
* For USDC transfers, this function is called directly by the
`TokenMessengerMinter` package in `deposit_for_burn()`.
**Parameters**
| Field | Type | Description |
| :------------------ | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer for executing the transaction |
| destination\_domain | `u32` | Destination domain identifier. |
| recipient | `address` | Recipient address. *Note: If destination is a non-Move chain,* `recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
| message\_body | `vector` | Message to be sent to destination chain |
#### [send\_message\_with\_caller](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L198)
This is the same as send\_message, except the `receive_message` call on the
destination domain must be called by `destination_caller`.
**Parameters**
| Field | Type | Description |
| :------------------ | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer for executing the transaction |
| destination\_domain | `u32` | Destination domain identifier. |
| recipient | `address` | Recipient address. *Note: If destination is a non-Move chain,* `recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
| destination\_caller | `address` | Address of caller on destination chain. |
| message\_body | `vector` | Message to be sent to destination chain |
**Destination Caller Notes**
If the `destination_caller` does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard `deposit_for_burn` should be preferred for
use cases where a specific destination caller is not required.
*Note: If destination is a non-Move chain,* `destination_caller` *address should
be converted to hex and passed in using the @0x123 address format.*
#### [replace\_message](https://github.com/circlefin/aptos-cctp/blob/master/packages/message_transmitter/sources/message_transmitter.move#L229)
Replace a message with a new message body and/or destination caller. The
originalAttestation must be a valid attestation of originalMessage, produced by
Circle's attestation service.
**Remarks:**
* Only the sender of the original deposit\_for\_burn transaction has access to
call `replace_message`
**Parameters**
| Field | Type | Description |
| :----------------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| caller | `Signer` | Signer executing the transaction |
| original\_message | `vector` | Original message bytes (to replace). |
| original\_attestation | `vector` | Original attestation |
| new\_message\_body | `Option>` | New message body |
| new\_destination\_caller | `Option` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. |
## Additional Notes
### Using Move Scripts
Pre-compiled scripts for executing `deposit_for_burn` and
`handle_receive_message` are available in the
[repo](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts)
for testnet and mainnet. Alternatively, the scripts can be compiled from the
source code. The documentation can be found on the official Aptos
[website](https://aptos.dev/en/build/smart-contracts/scripts/compiling-scripts).
### Mint Recipient Addresses for Aptos as Source Chain
Outgoing mint recipient addresses from Aptos are passed as Aptos address types
and can be treated the same as a `bytes32` mint recipient parameter on EVM
implementations.
### Mint Recipient Addresses for Aptos as Destination Chain
Aptos mint recipient addresses from other chains should be treated the same as a
hex `bytes32` parameter.
# CCTP API Hosts and Endpoints V1
Source: https://developers.circle.com/cctp/v1/cctp-apis
API hosts and endpoints for CCTP V1
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
CCTP V1 provides a set of API hosts and endpoints to manage messages,
attestations, and transaction details for your cross-chain USDC transfers.
## CCTP V1 API Service Hosts
| Environment | URL |
| :---------- | :------------------------------------ |
| **Testnet** | `https://iris-api-sandbox.circle.com` |
| **Mainnet** | `https://iris-api.circle.com` |
## CCTP V1 API Endpoints
CCTP V1 endpoints allow you to fetch attestations for **Standard Transfer** burn
events, verify public keys, and access transaction details. Below is an overview
of the CCTP V1 public endpoints. Click on any endpoint for its API reference.
| Endpoint | Description | Use Case |
| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| [`GET /v1/attestations/{messageHash}`](/api-reference/cctp/all/get-attestation) | Retrieves the signed attestation for a USDC burn event on the source chain. | Certifying the burn of USDC post hard finality. Used as a signal to mint USDC on the destination chain. |
| [`GET /v1/publicKeys`](/api-reference/cctp/all/get-public-keys) | Fetches Circle's active public keys for verifying attestation signatures. | Validating the authenticity of Circle's signed attestations. |
| [`GET /v1/messages/{sourceDomainId}/{transactionHash}`](/api-reference/cctp/all/get-messages) | Provides transaction details for burn events or associated messages. | Accessing detailed information about CCTP V1 transactions. |
**API Service Rate Limit**
The CCTP V1 API service rate limit is 35 requests per second. If you exceed 35
requests per second, the service blocks all API requests for the next 5 minutes
and returns an HTTP 429 response.
# CCTP Supported Blockchains V1
Source: https://developers.circle.com/cctp/v1/cctp-supported-blockchains
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
CCTP V1 is available on the following blockchains where USDC is natively issued,
providing **Standard Transfer** functionality.
**Mainnet:**
* Aptos
* Arbitrum
* Avalanche
* Base
* Ethereum
* Noble
* OP Mainnet
* Polygon PoS
* Solana
* Sui
* Unichain
**Testnet:**
* Aptos Testnet
* Arbitrum Sepolia
* Avalanche Fuji
* Base Sepolia
* Ethereum Sepolia
* Noble Testnet
* OP Sepolia
* Polygon PoS Amoy
* Solana Devnet
* Sui Testnet
* Unichain Sepolia
# CCTP EVM Contracts and Interfaces V1
Source: https://developers.circle.com/cctp/v1/evm-smart-contracts
CCTP V1 smart contracts for EVM-compatible blockchains
**This is CCTP V1 (Legacy) version. For the latest version, see
[CCTP](/cctp)**.
## Contract responsibilities
* **TokenMessenger**: Entrypoint for cross-chain USDC transfer. Routes messages
to burn USDC on a source chain, and mint USDC on a destination chain.
* **MessageTransmitter**: Generic message passing. Sends all messages on the
source chain, and receives all messages on the destination chain.
* **TokenMinter**: Responsible for minting and burning USDC. Contains
chain-specific settings used by burners and minters.
* **Message**: Provides helper functions for cross-chain transfers, such as
`bytes32ToAddress` and `addressToBytes32`, which are commonly used when
bridging between EVM and non-EVM chains. These conversions are simple: prepend
12 zero bytes to an EVM address, or strip them to convert back.
**Note:** If you're writing your own integration, it's more gas-efficient to
[include this logic directly in your
contract](https://github.com/circlefin/evm-cctp-contracts/blob/5f1901a9791b18204e8556bb53fb0dfcb05a832a/src/messages/Message.sol#L146)
rather than calling an external one.
Full contract source code is
[available on GitHub](https://github.com/circlefin/evm-cctp-contracts).
## Mainnet contract addresses
### TokenMessenger: Mainnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0xBd3fa81B58Ba92a82136038B25aDec7066af3155`](https://etherscan.io/address/0xbd3fa81b58ba92a82136038b25adec7066af3155) |
| **Avalanche** | 1 | [`0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982`](https://snowtrace.io/address/0x6b25532e1060ce10cc3b0a99e5683b91bfde6982) |
| **OP Mainnet** | 2 | [`0x2B4069517957735bE00ceE0fadAE88a26365528f`](https://optimistic.etherscan.io/address/0x2B4069517957735bE00ceE0fadAE88a26365528f) |
| **Arbitrum** | 3 | [`0x19330d10D9Cc8751218eaf51E8885D058642E08A`](https://arbiscan.io/address/0x19330d10D9Cc8751218eaf51E8885D058642E08A) |
| **Base** | 6 | [`0x1682Ae6375C4E4A97e4B583BC394c861A46D8962`](https://basescan.org/address/0x1682Ae6375C4E4A97e4B583BC394c861A46D8962) |
| **Polygon PoS** | 7 | [`0x9daF8c91AEFAE50b9c0E69629D3F6Ca40cA3B3FE`](https://polygonscan.com/address/0x9daf8c91aefae50b9c0e69629d3f6ca40ca3b3fe) |
| **Unichain** | 10 | [`0x4e744b28E787c3aD0e810eD65A24461D4ac5a762`](https://uniscan.xyz/address/0x4e744b28E787c3aD0e810eD65A24461D4ac5a762) |
### MessageTransmitter: Mainnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0x0a992d191DEeC32aFe36203Ad87D7d289a738F81`](https://etherscan.io/address/0x0a992d191deec32afe36203ad87d7d289a738f81) |
| **Avalanche** | 1 | [`0x8186359aF5F57FbB40c6b14A588d2A59C0C29880`](https://snowtrace.io/address/0x8186359af5f57fbb40c6b14a588d2a59c0c29880) |
| **OP Mainnet** | 2 | [`0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8`](https://optimistic.etherscan.io/address/0x4d41f22c5a0e5c74090899e5a8fb597a8842b3e8) |
| **Arbitrum** | 3 | [`0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca`](https://arbiscan.io/address/0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca) |
| **Base** | 6 | [`0xAD09780d193884d503182aD4588450C416D6F9D4`](https://basescan.org/address/0xAD09780d193884d503182aD4588450C416D6F9D4) |
| **Polygon PoS** | 7 | [`0xF3be9355363857F3e001be68856A2f96b4C39Ba9`](https://polygonscan.com/address/0xF3be9355363857F3e001be68856A2f96b4C39Ba9) |
| **Unichain** | 10 | [`0x353bE9E2E38AB1D19104534e4edC21c643Df86f4`](https://uniscan.xyz/address/0x353bE9E2E38AB1D19104534e4edC21c643Df86f4) |
### TokenMinter: Mainnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0xc4922d64a24675E16e1586e3e3Aa56C06fABe907`](https://etherscan.io/address/0xc4922d64a24675e16e1586e3e3aa56c06fabe907) |
| **Avalanche** | 1 | [`0x420F5035fd5dC62a167E7e7f08B604335aE272b8`](https://snowtrace.io/address/0x420f5035fd5dc62a167e7e7f08b604335ae272b8) |
| **OP Mainnet** | 2 | [`0x33E76C5C31cb928dc6FE6487AB3b2C0769B1A1e3`](https://optimistic.etherscan.io/address/0x33E76C5C31cb928dc6FE6487AB3b2C0769B1A1e3) |
| **Arbitrum** | 3 | [`0xE7Ed1fa7f45D05C508232aa32649D89b73b8bA48`](https://arbiscan.io/address/0xE7Ed1fa7f45D05C508232aa32649D89b73b8bA48) |
| **Base** | 6 | [`0xe45B133ddc64bE80252b0e9c75A8E74EF280eEd6`](https://basescan.org/address/0xe45B133ddc64bE80252b0e9c75A8E74EF280eEd6) |
| **Polygon PoS** | 7 | [`0x10f7835F827D6Cf035115E10c50A853d7FB2D2EC`](https://polygonscan.com/address/0x10f7835f827d6cf035115e10c50a853d7fb2d2ec) |
| **Unichain** | 10 | [`0x726bFEF3cBb3f8AF7d8CB141E78F86Ae43C34163`](https://uniscan.xyz/address/0x726bFEF3cBb3f8AF7d8CB141E78F86Ae43C34163) |
### Message: Mainnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| --------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Ethereum** | 0 | [`0xB2f38107A18f8599331677C14374Fd3A952fb2c8`](https://etherscan.io/address/0xb2f38107a18f8599331677c14374fd3a952fb2c8) |
| **Avalanche** | 1 | [`0x21F337db7A718F23e061262470Af8c1Fd01232D1`](https://snowtrace.io/address/0x21f337db7a718f23e061262470af8c1fd01232d1) |
| **OP Mainnet** | 2 | [`0xDB2831EaF163be1B564d437A97372deB0046C70D`](https://optimistic.etherscan.io/address/0xdb2831eaf163be1b564d437a97372deb0046c70d) |
| **Arbitrum** | 3 | [`0xE189BDCFbceCEC917b937247666a44ED959D81e4`](https://arbiscan.io/address/0xe189bdcfbcecec917b937247666a44ed959d81e4) |
| **Base** | 6 | [`0x827ae40E55C4355049ab91e441b6e269e4091441`](https://basescan.org/address/0x827ae40E55C4355049ab91e441b6e269e4091441) |
| **Polygon PoS** | 7 | [`0x02d9fa3e7f870E5FAA7Ca6c112031E0ddC5E646C`](https://polygonscan.com/address/0x02d9fa3e7f870E5FAA7Ca6c112031E0ddC5E646C) |
| **Unichain** | 10 | [`0x395b1be6E432033B676e3e36B2c2121a1f952622`](https://uniscan.xyz/address/0x395b1be6E432033B676e3e36B2c2121a1f952622) |
## Testnet contract addresses
### TokenMessenger: Testnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ethereum Sepolia** | 0 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia.etherscan.io/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5) |
| **Avalanche Fuji** | 1 | [`0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0`](https://testnet.snowtrace.io/address/0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0) |
| **OP Sepolia** | 2 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia-optimism.etherscan.io/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) |
| **Arbitrum Sepolia** | 3 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://sepolia.arbiscan.io/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) |
| **Base Sepolia** | 6 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://base-sepolia.blockscout.com/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5) |
| **Polygon PoS Amoy** | 7 | [`0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5`](https://amoy.polygonscan.com/address/0x9f3b8679c73c2fef8b59b4f3444d4e156fb70aa5) |
| **Unichain Sepolia** | 10 | [`0x8ed94B8dAd2Dc5453862ea5e316A8e71AAed9782`](https://unichain-sepolia.blockscout.com/address/0x8ed94B8dAd2Dc5453862ea5e316A8e71AAed9782) |
### MessageTransmitter: Testnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ethereum Sepolia** | 0 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://sepolia.etherscan.io/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) |
| **Avalanche Fuji** | 1 | [`0xa9fB1b3009DCb79E2fe346c16a604B8Fa8aE0a79`](https://testnet.snowtrace.io/address/0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79) |
| **OP Sepolia** | 2 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://sepolia-optimism.etherscan.io/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) |
| **Arbitrum Sepolia** | 3 | [`0xaCF1ceeF35caAc005e15888dDb8A3515C41B4872`](https://sepolia.arbiscan.io/address/0xacf1ceef35caac005e15888ddb8a3515c41b4872) |
| **Base Sepolia** | 6 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://base-sepolia.blockscout.com/address/0x7865fAfC2db2093669d92c0F33AeEF291086BEFD) |
| **Polygon PoS Amoy** | 7 | [`0x7865fAfC2db2093669d92c0F33AeEF291086BEFD`](https://amoy.polygonscan.com/address/0x7865fafc2db2093669d92c0f33aeef291086befd) |
| **Unichain Sepolia** | 10 | [`0xbc498c326533d675cf571B90A2Ced265ACb7d086`](https://unichain-sepolia.blockscout.com/address/0xbc498c326533d675cf571B90A2Ced265ACb7d086) |
### TokenMinter: Testnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ethereum Sepolia** | 0 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia.etherscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) |
| **Avalanche Fuji** | 1 | [`0x4ED8867f9947A5fe140C9dC1c6f207F3489F501E`](https://testnet.snowtrace.io/address/0x4ed8867f9947a5fe140c9dc1c6f207f3489f501e) |
| **OP Sepolia** | 2 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia-optimism.etherscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) |
| **Arbitrum Sepolia** | 3 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://sepolia.arbiscan.io/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) |
| **Base Sepolia** | 6 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://base-sepolia.blockscout.com/address/0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A) |
| **Polygon PoS Amoy** | 7 | [`0xE997d7d2F6E065a9A93Fa2175E878Fb9081F1f0A`](https://amoy.polygonscan.com/address/0xe997d7d2f6e065a9a93fa2175e878fb9081f1f0a) |
| **Unichain Sepolia** | 10 | [`0x7348358C94519Da790DB38638d8c23669d343Bc6`](https://unichain-sepolia.blockscout.com/address/0x7348358C94519Da790DB38638d8c23669d343Bc6) |
### Message: Testnet
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| -------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ethereum Sepolia** | 0 | [`0x80537e4e8bAb73D21096baa3a8c813b45CA0b7c9`](https://sepolia.etherscan.io/address/0x80537e4e8bAb73D21096baa3a8c813b45CA0b7c9) |
| **Avalanche Fuji** | 1 | [`0xeAf1DB5E3eb86FEbD8080368a956622b62Dcb78f`](https://testnet.snowtrace.io/address/0xeaf1db5e3eb86febd8080368a956622b62dcb78f) |
| **OP Sepolia** | 2 | [`0xffbeA106ce4A3CdAfcC82BAebeD78C81814e32Ed`](https://sepolia-optimism.etherscan.io/address/0xffbeA106ce4A3CdAfcC82BAebeD78C81814e32Ed) |
| **Arbitrum Sepolia** | 3 | [`0x70fAB9868cd54E12C7d87196424d6E0ca21be534`](https://sepolia.arbiscan.io/address/0x70fAB9868cd54E12C7d87196424d6E0ca21be534) |
| **Base Sepolia** | 6 | [`0x8E52a9e76148185536F0f0779749Cc895E5f70dC`](https://base-sepolia.blockscout.com/address/0x8E52a9e76148185536F0f0779749Cc895E5f70dC) |
| **Polygon PoS Amoy** | 7 | [`0x8E52a9e76148185536F0f0779749Cc895E5f70dC`](https://amoy.polygonscan.com/address/0x8E52a9e76148185536F0f0779749Cc895E5f70dC) |
| **Unichain Sepolia** | 10 | [`0x1Fae490d95dDcFFD70728AF5024C524ed303a2e3`](https://unichain-sepolia.blockscout.com/address/0x1Fae490d95dDcFFD70728AF5024C524ed303a2e3) |
## CCTP V1 Interface
This section provides the **CCTP V1 Smart Contract Interface** exposed by **CCTP
V1**, outlining the available functions, and their parameters.
The interface below serves as a reference for permissionless messaging functions
exposed by the **TokenMessenger** and **MessageTransmitter** functions. The full
ABIs are
[available on GitHub](https://github.com/circlefin/evm-cctp-contracts/tree/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/docs/abis/cctp).
### TokenMessenger
#### depositForBurn
Deposits and burns tokens from sender to be minted on destination domain. Minted
tokens will be transferred to `mintRecipient`.
**Parameters**
| Field | Type | Description |
| ------------------- | --------- | ------------------------------------------------------------ |
| `amount` | `uint256` | Amount of tokens to deposit and burn |
| `destinationDomain` | `uint32` | Destination domain identifier |
| `mintRecipient` | `bytes32` | Address of mint recipient on destination domain |
| `burnToken` | `address` | Address of contract to burn deposited tokens on local domain |
#### depositForBurnWithCaller
Same as `depositForBurn` but with an additional parameter, `destinationCaller`.
This parameter specifies which address has permission to call `receiveMessage`
on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| ------------------- | --------- | ------------------------------------------------------------ |
| `amount` | `uint256` | Amount of tokens to deposit and burn |
| `destinationDomain` | `uint32` | Destination domain identifier |
| `mintRecipient` | `bytes32` | Address of mint recipient on destination domain |
| `burnToken` | `address` | Address of contract to burn deposited tokens on local domain |
| `destinationCaller` | `bytes32` | Address of caller on the destination domain |
#### replaceDepositForBurn
Replace a `BurnMessage` to change the mint recipient and/or destination caller.
Allows the sender of a previous `BurnMessage` (created by `depositForBurn` or
`depositForBurnWithCaller`) to send a new `BurnMessage` to replace the original.
The new `BurnMessage` will reuse the amount and burn token of the original,
without requiring a new deposit.
This is useful in situations where the user specified an incorrect address and
has no way to safely mint the previously burned USDC.
The sender of the original `depositForBurn` has access to call
`replaceDepositForBurn`. The resulting mint will supersede the original mint,
as long as the original mint has not confirmed yet onchain. When using a
third-party app/bridge that integrates with CCTP V1 to burn and mint USDC, it
is the choice of the app/bridge if and when to replace messages on behalf of
users. When sending USDC to smart contracts, be aware of the functionality
that those contracts have and their respective trust model.
**Parameters**
| Field | Type | Description |
| ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `originalMessage` | `bytes calldata` | Original message bytes (to replace) |
| `originalAttestation` | `bytes calldata` | Original attestation bytes |
| `newDestinationCaller` | `bytes32` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid |
| `newMintRecipient` | `bytes32` | The new mint recipient, which may be the same as the original mint recipient, or different |
### MessageTransmitter
#### receiveMessage
Messages with a given nonce can only be broadcast successfully once for a pair
of domains. The message body of a valid message is passed to the specified
recipient for further processing.
**Parameters**
| Field | Type | Description |
| ------------- | ---------------- | ----------------------------- |
| `message` | `bytes calldata` | Message bytes |
| `attestation` | `bytes calldata` | Signed attestation of message |
#### sendMessage
Sends a message to the destination domain and recipient. Emits a `MessageSent`
event which will be attested by Circle's attestation service (Iris).
**Parameters**
| Field | Type | Description |
| ------------------- | ---------------- | ---------------------------------------------------- |
| `destinationDomain` | `uint32` | Destination domain identifier |
| `recipient` | `bytes32` | Address to handle message body on destination domain |
| `messageBody` | `bytes calldata` | App-specific message to be handled by recipient |
#### sendMessageWithCaller
Same as `sendMessage` but with an additional parameter, `destinationCaller`.
This parameter specifies which address has permission to call `receiveMessage`
on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| ------------------- | ---------------- | -------------------------------------------------- |
| `destinationDomain` | `uint32` | Destination domain identifier |
| `recipient` | `bytes32` | Address of message recipient on destination domain |
| `destinationCaller` | `bytes32` | Address of caller on the destination domain |
| `messageBody` | `bytes calldata` | App-specific message to be handled by recipient |
#### replaceMessage
Replace a message with a new message body and/or destination caller. The
`originalAttestation` must be a valid attestation of `originalMessage`, produced
by Circle's attestation service (Iris).
**Parameters**
| Field | Type | Description |
| ---------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `originalMessage` | `bytes calldata` | Original message to replace |
| `originalAttestation` | `bytes calldata` | Attestation of `originalMessage` |
| `newMessageBody` | `bytes calldata` | New message body of replaced message |
| `newDestinationCaller` | `bytes32` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller (bytes32(0), indicating that any destination caller is valid) |
# CCTP Message Passing V1
Source: https://developers.circle.com/cctp/v1/generic-message-passing
CCTP V1 architecture on EVM and non-EVM domains
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
Cross-Chain Transfer Protocol V1 uses generalized message passing to facilitate
the native burning and minting of USDC across supported blockchains, also known
as [domains](/cctp/v1/supported-domains). Message passing is a three-step
process:
1. An onchain component on the source domain emits a message.
2. Circle's offchain attestation service signs the message.
3. The onchain component at the destination domain receives the message, and
forwards the message body to the specified recipient.
## Architecture
Onchain components on all domains have the same purpose, but implementation
differs between EVM-compatible and non-EVM domains.
### CCTP V1 on EVM Domains
The relationship between CCTP V1's onchain components and Circle's offchain
Attestation Service is illustrated below for a burn-and-mint of USDC between
EVM-compatible domains:
On EVM domains, the onchain component for cross-chain burning and minting is
called **TokenMessenger**, which is built on top of **MessageTransmitter**, an
onchain component for generalized message passing.
In the diagram above, a token depositor calls the
[TokenMessenger#depositForBurn](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/TokenMessenger.sol#L169)
function to deposit a native token (such as USDC), which delegates to the
**TokenMinter** contract to burn the token. The **TokenMessenger** contract then
sends a message via the
[MessageTransmitter#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L108)
function. After
[sufficient block confirmations](/cctp/v1/required-block-confirmations),
Circle's offchain attestation service, Iris, signs the message. An API consumer
queries this attestation and submits it onchain to the destination domain's
[MessageTransmitter#receiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L250)
function. For more details, see
[Quickstart: Cross-chain USDC transfer](/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche).
To send an arbitrary message, directly call
[MessageTransmitter#sendMessage](https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L108).
Note that the message recipient must implement
[IMessageHandler#handleReceiveMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/interfaces/IMessageHandler.sol#L31C14-L31C34).
**Note:**
In CCTP V1, it is not possible to perform a burn-and-mint operation for USDC and
include arbitrary data in the same message. You must include arbitrary data in a
separate message. In later CCTP versions, you can burn-and-mint while including
data in the same message via Hooks.
### CCTP V1 on Non-EVM Domains
#### Noble
Noble is a Cosmos application-specific blockchain (or "appchain") that provides
native asset issuance for the Cosmos ecosystem. USDC is natively issued on Noble
and can be transferred via the Inter-Blockchain Communication (IBC) protocol to
other supported appchains in Cosmos, or via CCTP V1 to any supported domain (for
example, Ethereum).
Note that there are key differences between Cosmos appchains like Noble and
EVM-compatible blockchains. Unlike on EVM domains where CCTP V1 is a set of
smart contracts, CCTP V1 on Noble is a Cosmos SDK module, which is deployed by
Noble governance and built into the Noble blockchain. Cosmos appchains can use
IBC to build composable flows with CCTP V1 on Noble. Refer to the
[Noble documentation](/cctp/v1/noble-cosmos-module) for more details.
#### Solana
Solana is a layer-1 blockchain where USDC is natively issued as an SPL-token.
CCTP V1 is deployed to Solana as two Anchor programs: **MessageTransmitter** and
**TokenMessengerMinter**. Developers can compose programs on top of CCTP V1
programs through CPI's (Cross-Program Invocations). Arbitrary messages can be
sent directly by calling `MessageTransmitter#send_message` just as described in
the EVM section above. Refer to the
[Solana documentation](/cctp/v1/solana-programs) for more details.
#### Sui
Sui is another layer-1 blockchain where USDC is natively issued as a
[`Coin` implementation](https://docs.sui.io/guides/developer/coin). CCTP V1 is
deployed to Sui as two programs: **MessageTransmitter** and
**TokenMessengerMinter**. Arbitrary messages can be sent by directly calling
`message_transmitter::send_message` similar to the EVM section above. Refer to
the [Sui documentation](/cctp/v1/sui-packages) for more details.
#### Aptos
Aptos is another layer-1 blockchain where USDC is natively issued as a
[`FA` implementation](https://aptos.dev/en/build/smart-contracts/fungible-asset).
CCTP V1 is deployed to Aptos as two programs: **MessageTransmitter** and
**TokenMessengerMinter**. Arbitrary messages can be sent by directly calling
`message_transmitter::send_message` similar to the EVM section above. Refer to
the [Aptos documentation](/cctp/v1/aptos-packages) for more details.
# CCTP Limits V1
Source: https://developers.circle.com/cctp/v1/limits
Limits for CCTP V1 burning and minting
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
## Minter Allowance
The USDC smart contract (or module) on each blockchain specifies a limit for how
much USDC can be minted before the limit needs to be increased by the master
minter, Circle. This limit is called the "minter allowance" and it is
individually set for each authorized minter, such as CCTP V1.
Minter allowance is decremented each time the authorized minter mints, by the
amount of USDC that is minted. A transaction attempting to mint in excess of the
minter allowance will fail, but may succeed on a subsequent retry after the
minter allowance is reset. Minter allowance can be queried from the USDC
contract on EVM-compatible chains using the public
[minterAllowance](https://github.com/centrehq/centre-tokens/blob/0d3cab14ebd133a83fc834dbd48d0468bdf0b391/contracts/v1/FiatTokenV1.sol#L153)
function. For CCTP V1 on Noble, minter allowance can be queried via the
[fiattokenfactory module minters API](https://github.com/circlefin/noble-fiattokenfactory/blob/33b30a6cf87eba20874df84fa93dd100f71ed512/proto/fiattokenfactory/query.proto#L49-L52).
## Per-Message Burn Limit
CCTP V1 defines per-message burn limits. This value is configurable by Circle.
This limit prevents the situation where a user burns an amount of USDC on a
source chain that could never be minted on a destination chain without
increasing minter allowance thresholds. Per-message burn limits can be queried
on the TokenMinter contract on EVM-compatible chains, using the public
[burnLimitsPerMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/roles/TokenController.sol#L69)
mapping. For CCTP V1 on Noble, the per-message burn limit can be queried via the
[cctp module per\_message\_burn\_limits API](https://github.com/circlefin/noble-fiattokenfactory/blob/master/proto/fiattokenfactory/query.proto#L49-L52).
# CCTP Message Format V1
Source: https://developers.circle.com/cctp/v1/message-format
Format arbitrary and application-specific messages using CCTP V1
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
## CCTP V1 Message Header
The top-level message header format is standard for all messages passing through
CCTP V1.
| Field | Offset | Solidity Type | Length (bytes) | Description |
| ------------------- | ------ | ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `version` | 0 | uint32 | 4 | Version identifier - use 0 for CCTP V1 |
| `sourceDomain` | 4 | uint32 | 4 | Source domain ID |
| `destinationDomain` | 8 | uint32 | 4 | Destination domain ID |
| `nonce` | 12 | uint64 | 8 | Unique message nonce (see [Sequential Nonces](#sequential-nonces)) |
| `sender` | 20 | bytes32 | 32 | Address of MessageTransmitter caller on source domain |
| `recipient` | 52 | bytes32 | 32 | Address to handle message body on destination domain |
| `destinationCaller` | 84 | bytes32 | 32 | Address permitted to call MessageTransmitter on destination domain, or bytes32(0) if message can be received by any address |
| `messageBody` | 116 | bytes | dynamic | Application-specific message to be handled by recipient |
### Sequential Nonces
A message nonce is a unique identifier for a message that can only be used once
on the destination domain. In CCTP V1, message nonces are implemented using
**Sequential Nonces**, where the next available nonce on a source domain is an
integer. On the destination domain, messages can be received in any order, and
used nonces are stored as a hash of the source domain and nonce integer value.
**Why we use `bytes32` type for addresses**
CCTP V1 is built to support EVM chains, which use 20 byte addresses, and non-EVM
chains, many of which use 32 byte addresses. We provide a
[Message.sol library](https://github.com/circlefin/evm-cctp-contracts/blob/40111601620071988e94e39274c8f48d6f406d6d/src/messages/Message.sol#L145-L157)
as a reference implementation for converting between address and `bytes32` in
Solidity.
## CCTP V1 Message Body
The message format includes a dynamically sized `messageBody` field, used for
application-specific messages. For example, TokenMessenger defines a
[BurnMessage](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/BurnMessage.sol)
with data related to cross-chain transfers.
| Field | Offset | Solidity Type | Length (bytes) | Description |
| --------------- | ------ | ------------- | -------------- | -------------------------------------------------------------------------------------- |
| `version` | 0 | uint32 | 4 | Version identifier (0, for CCTP V1) |
| `burnToken` | 4 | bytes32 | 32 | Address of burned token on source domain |
| `mintRecipient` | 36 | bytes32 | 32 | Address to receive minted tokens on destination domain |
| `amount` | 68 | uint256 | 32 | Amount of burned tokens |
| `messageSender` | 100 | bytes32 | 32 | Address of caller of `depositForBurn` (or `depositForBurnWithCaller`) on source domain |
# CCTP Noble Cosmos Module V1
Source: https://developers.circle.com/cctp/v1/noble-cosmos-module
Cosmos SDK Module for CCTP V1 support on Noble
**This is CCTP (Legacy) version. For the latest version, see [CCTP](/cctp)**.
## Overview
Noble is a Cosmos application-specific blockchain (or "appchain") that provides
native asset issuance for the Cosmos ecosystem. USDC is natively issued on Noble
and can be transferred via the Inter-Blockchain Communication Protocol (IBC) to
other supported appchains in Cosmos, or via CCTP V1 to any supported domain (for
example, Ethereum).
Note that there are key differences between Cosmos appchains like Noble and
EVM-compatible blockchains. Unlike on EVM chains where CCTP V1 is a set of smart
contracts, CCTP V1 on Noble is a Cosmos SDK module, which is deployed by Noble
governance and built into the Noble blockchain. Cosmos appchains can use IBC to
build composable flows with CCTP V1 on Noble.
## Testnet and Mainnet Module Address
| Chain | [Domain](/cctp/v1/supported-domains) | Address |
| :---- | :----------------------------------- | :------------------------------------------- |
| Noble | 4 | noble12l2w4ugfz4m6dd73yysz477jszqnfughxvkss5 |
CCTP V1 on Noble source code is
[available on GitHub](https://github.com/circlefin/noble-cctp). The full message
spec is defined at
[noble-cctp/x/cctp/spec/02\_messages.md](https://github.com/circlefin/noble-cctp/blob/dc81b3e0d566d195c869a213519fcecd38b020a5/x/cctp/spec/02_messages.md).
The interface below serves as a reference for permissionless messaging functions
exposed by the module.
## Module Interface
### depositForBurn
**Message**: `MsgDepositForBurn`
Broadcast a transaction that deposits for burn to a provided domain.
**Arguments**:
* `Amount` - The burn amount
* `DestinationDomain` - Domain of destination chain
* `MintRecipient` - address receiving minted tokens on destination chain as a 32
length byte array
* `BurnToken` - The burn token address on source domain
### depositForBurnWithCaller
**Message**:`MsgDepositForBurnWithCaller`
Broadcast a transaction that deposits for burn with caller to a provided domain.
This message wraps `MsgDepositForBurn`. It adds one extra argument,
`destinationCaller`.
**Arguments**:
* `Amount` - The burn amount
* `DestinationDomain` - Domain of destination chain
* `MintRecipient` - address receiving minted tokens on destination chain as a 32
length byte array
* `BurnToken` - The burn token address on source domain
* `DestinationCaller` - authorized caller as 32 length byte array of
receiveMessage() on destination domain
### replaceDepositForBurn
**Message**: `MsgReplaceDepositForBurn`
Broadcast a transaction that replaces a deposit for burn message. Replace the
mint recipient and/or\
destination caller.
Allows the sender of a previous BurnMessage (created by depositForBurn or
depositForBurnWithCaller)\
to send a new BurnMessage to replace the original. The new BurnMessage will
reuse the amount and\
burn token of the original without requiring a new deposit.
**Arguments**:
* `OriginalMessage`- original message bytes to replace
* `OriginalAttestation`- attestation bytes of `OriginalMessage`
* `NewDestinationCaller` - the new destination caller, which may be the\
same as the original destination caller, a new destination caller, or an
empty\
destination caller, indicating that any destination caller is valid.
* `NewMintRecipient` - the new mint recipient. May be the same as the\
original mint recipient, or different.
### receiveMessage
**Message**: `MsgReceiveMessage`
Broadcast a transaction that receives a provided message from another domain.
After validation, it performs a mint.
**Arguments**:
* `message` [Message Format](/cctp/v1/message-format)
* `attestation` - Concatenated 65-byte signature(s) of `message`, in increasing
order\
of the attester address recovered from signatures.
### sendMessage
**Message**:`MsgSendMessage`
Broadcast a transaction that sends a message to a provided domain.
**Arguments**:
* `DestinationDomain` - Domain of destination chain
* `Recipient` - Address of message recipient on destination chain
* `MessageBody` - Raw bytes content of message
### sendMessageWithCaller
**Message**:`MsgSendMessageWithCaller`
Broadcast a transaction that sends a message with a caller to a provided domain.
Specifying a Destination caller requires that only the specified caller can call
`receiveMessage()` on destination domain.
This message wraps `SendMessage` It adds one extra argument,
`DestinationCaller`.
**Arguments**:
* `DestinationDomain` - Domain of destination chain
* `Recipient` - Address of message recipient on destination chain
* `MessageBody` - Raw bytes content of message
* `DestinationCaller` - caller on the destination domain, as 32 length byte
array
### replaceMessage
**Message**: `MsgReplaceMessage`
Broadcast a transaction that replaces a provided message. Replace the message
body and/or destination caller.
**Arguments**:
* `OriginalMessage` - original message bytes to replace
* `OriginalAttestation` - attestation bytes of `OriginalMessage`
* `NewMessageBody` - new message body of replaced message
* `NewDestinationCaller` - the new destination caller, which may be the same as
the original destination caller, a new destination caller, or an empty
destination caller, indicating that any destination caller is valid.
# CCTP Block Confirmations V1
Source: https://developers.circle.com/cctp/v1/required-block-confirmations
Block confirmation requirements for attestations by chain
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
Before signing an attestation for a source chain event, Circle waits for a
specified number of onchain block confirmations to achieve
hard finality.
The table below shows the average time required for an attestation to become
available after a message is emitted onchain.
**Note:** These values are subject to change.
## CCTP V1 Attestation Times
| Source Chain | Number of Blocks | Average Time |
| --------------- | ----------------- | -------------------- |
| **Ethereum** | \~65\* | \~13 to 19 minutes\* |
| **Avalanche** | 1 | \~8 seconds |
| **OP Mainnet** | \~65 ETH blocks\* | \~13 to 19 minutes\* |
| **Arbitrum** | \~65 ETH blocks\* | \~13 to 19 minutes\* |
| **Noble** | 1 | \~20 seconds |
| **Base** | \~65 ETH blocks\* | \~13 to 19 minutes\* |
| **Polygon PoS** | \~33 | \~75 to 120 seconds |
| **Solana** | 32 | \~25 seconds |
| **Sui** | 1 | \~8 seconds |
| **Aptos** | 1 | \~8 seconds |
| **Unichain** | \~65 ETH blocks\* | \~13 to 19 minutes\* |
**Block confirmations for L2s to Ethereum**
Layer 2 (L2) blockchains publish transaction data in batches to Ethereum L1, and
the frequency of these posts varies by chain. Some submit batches every few
minutes, while others are less frequent. After a batch is posted, Circle waits
for the Ethereum L1 block containing the batch to finalize, which typically
happens after \~65 blocks, or 13 to 19 minutes, before issuing an attestation.
# CCTP Solana Programs and Interfaces V1
Source: https://developers.circle.com/cctp/v1/solana-programs
Programs for CCTP V1 support on the Solana blockchain
**This is CCTP V1 (Legacy) version. For the latest version, see
[CCTP](/cctp)**.
## Overview
Solana CCTP V1 programs are written in Rust and leverage the Anchor framework.
The Solana CCTP V1 protocol implementation is split into two programs:
`MessageTransmitter` and `TokenMessengerMinter`. `TokenMessengerMinter`
encapsulates the functionality of both `TokenMessenger` and `TokenMinter`
contracts on EVM chains. To ensure alignment with EVM contracts' logic and
state, and to facilitate future upgrades and maintenance, the code and state of
Solana programs reflect the EVM counterparts as closely as possible.
### Mainnet Program Addresses
| Program | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| MessageTransmitter | 5 | [`CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd`](https://solscan.io/account/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd) |
| TokenMessengerMinter | 5 | [`CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3`](https://solscan.io/account/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3) |
### Devnet Program Addresses
| Program | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| MessageTransmitter | 5 | [`CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd`](https://solscan.io/account/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd?cluster=devnet) |
| TokenMessengerMinter | 5 | [`CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3`](https://solscan.io/account/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3?cluster=devnet) |
The Solana CCTP V1 source code is
[available on GitHub](https://github.com/circlefin/solana-cctp-contracts/). The
interface below serves as a reference for permissionless messaging functions
exposed by the programs.
## CCTP V1 Interface
The interface below serves as a reference for permissionless messaging functions
exposed by the `TokenMessengerMinter` and `MessageTransmitter` programs. The
full IDLs can be found onchain using a block explorer.
[MessageTransmitter IDL](https://explorer.solana.com/address/CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3/anchor-program)
and
[TokenMessengerMinter IDL](https://explorer.solana.com/address/CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd/anchor-program).
*Please see the instruction rust files or quick-start for PDA information.*
### TokenMessengerMinter
### [depositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/deposit_for_burn.rs)
Deposits and burns tokens from sender to be minted on destination domain. Minted
tokens will be transferred to `mintRecipient`.
**Parameters**
| Field | Type | Description |
| :---------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| amount | u64 | Amount of tokens to deposit and burn. |
| destinationDomain | u32 | Destination domain identifier. |
| mintRecipient | Pubkey | Public Key of token account mint recipient on destination domain. *Address should be the 32 byte version of the hex address in base58. See Additional Notes on `mintRecipient` section for more information.* |
**MessageSent event storage**
To ensure persistent and reliable message storage, MessageSent events are stored
in accounts. MessageSent event accounts are generated client-side, passed into
the instruction call, and assigned to have the `MessageTransmitter` program as
the owner. Please see the
[Quickstart Guide](/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche)
for how to generate this account and pass it to the instruction call.
For `depositForBurn` messages, this costs `~0.00295104 SOL` in rent. *This rent
is paid by the
[`event_rent_payer`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message.rs#L16C9-L16C25)
account which can be the user or subsidized by a calling program or integrator.*
Once an attestation is available and the message has been received on the
destination chain, the event account can be closed and have the SOL reclaimed to
the `event_rent_payer` account. This is done by calling the
[`reclaim_event_account`](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/reclaim_event_account.rs)
instruction. This can only be called by the `event_rent_payer` account from when
the message was sent.
Details on the message format can be found on the
[Message Format page](/cctp/v1/message-format).
### [depositForBurnWithCaller](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/deposit_for_burn_with_caller.rs)
The same as `depositForBurn` but with an additional parameter,
`destinationCaller`. This parameter specifies which address has permission to
call `receiveMessage` on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| :---------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| amount | u64 | Amount of tokens to deposit and burn. |
| destinationDomain | u32 | Destination domain identifier. |
| mintRecipient | Pubkey | Public Key of mint recipient on destination domain. *Address should be converted to base58.* See \[Mint Recipient for Solana as Source Chain Transfers]\(Mint Recipient for Solana as Source Chain Transfers) |
| destinationCaller | Pubkey | Public Key of caller on destination domain. *Address should be converted to base58.* |
### [replaceDepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/instructions/replace_deposit_for_burn.rs)
Replace a `BurnMessage` to change the mint recipient and/or destination caller.
Allows the sender of a previous `BurnMessage` (created by `depositForBurn` or
`depositForBurnWithCaller`) to send a new `BurnMessage` to replace the original.
The new `BurnMessage` will reuse the amount and burn token of the original,
without requiring a new deposit.
This is useful in situations where the user specified an incorrect address and
has no way to safely mint the previously burned USDC.
**Note on replaceDepositForBurn**
Only the owner account of the original depositForBurn has access to call
replaceDepositForBurn. The resulting mint will supersede the original mint, as
long as the original mint has not confirmed yet onchain. When using a
third-party app/bridge that integrates with CCTP V1 to burn and mint USDC, it is
the choice of the app/bridge if and when to replace messages on behalf of users.
When sending USDC to smart contracts, be aware of the functionality that those
contracts have and their respective trust model.
**Parameters**
| Field | Type | Description |
| :------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| originalMessage | Vec\ | Original message bytes (to replace). |
| originalAttestation | Vec\ | Original attestation bytes. |
| newDestinationCaller | Pubkey | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. *Address should be converted to base58.* |
| newMintRecipient | Pubkey | The new mint recipient, which may be the same as the original mint recipient, or different. *Address should be converted to base58.* |
### MessageTransmitter
### [receiveMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/receive_message.rs)
Messages with a given nonce can only be broadcast successfully once for a pair
of domains. The message body of a valid message is passed to the specified
recipient for further processing.
**Parameters**
| Field | Type | Description |
| :---------- | :------- | :----------------------------- |
| message | Vec\ | Message bytes. |
| attestation | Vec\ | Signed attestation of message. |
**Remaining Accounts**
If the `receiveMessage` instruction is being called with a deposit for burn
message that will be received by the `TokenMessengerMinter`, additional
`remainingAccounts` are required so they can be passed with the CPI to
`TokenMessengerMinter#handleReceiveMessage`:
| Account Name | PDA Seeds | PDA ProgramId | isSigner? | isWritable? | Description |
| :------------------------------ | :---------------------------------------------------- | :------------------- | :-------- | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_messenger` | `["token_messenger"]` | tokenMessengerMinter | false | false | TokenMessenger Program Account |
| `remote_token_messenger` | `["remote_token_messenger", sourceDomainId]` | tokenMessengerMinter | false | false | Remote token messenger account where the remote token messenger address is stored for the given source domain id |
| `token_minter` | `["token_minter"]` | tokenMessengerMinter | false | true | TokenMinter Program Account |
| `local_token` | `["local_token", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Local token account where the information for the local token (e.g. USDCSOL) being minted is stored |
| `token_pair` | `["token_pair", sourceDomainId, sourceTokenInBase58]` | tokenMessengerMinter | false | false | Token pair account where the info for the local and remote tokens are stored. `sourceTokenInBase58` is the remote token that was burned converted into base58 format. |
| `user_token_account` | N/A | N/A | false | true | User token account that will receive the minted tokens. This address **must** match the mintRecipient from the source chain depositForBurn call. |
| `custody_token_account` | `["custody", localTokenMint.publicKey]` | tokenMessengerMinter | false | true | Custody account that holds the pre-minted USDCSOL that can be minted for CCTP V1 usage. |
| `SPL.token_program_id` | N/A | N/A | false | false | The native SPL token program ID. |
| `token_program_event_authority` | `["__event_authority"]` | tokenMessengerMinter | false | false | Event authority account for the TokenMessengerMinter program. Needed to emit Anchor CPI events. |
| `program` | N/A | N/A | false | false | Program id for the TokenMessengerMinter program. |
### [sendMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message.rs)
Sends a message to the destination domain and recipient. Emits a `MessageSent`
event which will be attested by Circle's attestation service.
**Parameters**
| Field | Type | Description |
| :---------------- | :------- | :------------------------------------------------------- |
| destinationDomain | u32 | Destination domain identifier. |
| recipient | Pubkey | Address to handle message body on destination domain. |
| messageBody | Vec\ | Application-specific message to be handled by recipient. |
### [sendMessageWithCaller](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/send_message_with_caller.rs)
Same as `sendMessage` but with an additional parameter, `destinationCaller`.
This parameter specifies which address has permission to call `receiveMessage`
on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| :---------------- | :------- | :------------------------------------------------------------------------------------ |
| destinationDomain | u32 | Destination domain identifier. |
| recipient | Pubkey | Address of message recipient on destination domain. |
| destinationCaller | Pubkey | Address of caller on the destination domain. *Address should be converted to base58.* |
| messageBody | Vec\ | Application-specific message to be handled by recipient. |
### [replaceMessage](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/replace_message.rs)
Replace a message with a new message body and/or destination caller. The
`originalAttestation` must be a valid attestation of `originalMessage`, produced
by Circle's attestation service.
**Parameters**
| Field | Type | Description |
| :------------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| originalMessage | Vec\ | Original message to replace. |
| originalAttestation | Vec\ | Attestation of originalMessage. |
| newMessageBody | Vec\ | New message body of replaced message. |
| newDestinationCaller | Pubkey | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller (bytes32(0) or `PublicKey.default`, indicating that any destination caller is valid). *Address should be converted to base58.* |
### [reclaimEventAccount](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/instructions/reclaim_event_account.rs)
Closes the given event account and reclaims the paid rent in SOL back to the
`event_rent_payer` account. This instruction can only be called by the
`event_rent_payer` account that paid the rent when the message was sent.
**Parameters**
| Field | Type | Description |
| :---------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| attestation | Vec\ | Valid attestation for the message stored in the account. This is required to ensure the attestation service has processed and stored the message before it is deleted. |
**Warning**
Once this instruction is executed for a message account, the message can no
longer be read onchain. We recommend not calling this instruction until the
message has been received on the destination chain. If the message is lost
before receiving the message, it can be fetched from the attestation service
using the [messages endpoint](/api-reference/cctp/all/get-messages).
## Additional Notes
### Mint Recipient for Solana as Destination Chain Transfers
When calling `depositForBurn` on a non-Solana chain with Solana as the
destination, the `mintRecipient` should be a **hex encoded USDC token account
address**. The token account\* must exist at the time `receiveMessage` is called
on Solana\* or else this instruction will revert. An example of converting an
address from Base58 to hex taken from the Solana quickstart tutorial in
TypeScript can be seen below:
```typescript TypeScript theme={null}
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { hexlify } from "ethers";
const solanaAddressToHex = (solanaAddress: string): string =>
hexlify(bs58.decode(solanaAddress));
```
### Mint Recipient for Solana as Source Chain Transfers
When specifying the `mintRecipient` for Solana `deposit_for_burn` instruction
calls, the address must be given as the 32 byte version of the hex address in
base58 format. An example taken from the Solana quickstart tutorial in
TypeScript can be seen below:
```typescript TypeScript theme={null}
import { getBytes } from "ethers";
import { PublicKey } from "@solana/web3.js";
const evmAddressToBytes32 = (address: string): string =>
`0x000000000000000000000000${address.replace("0x", "")}`;
const evmAddressToBase58PublicKey = (addressHex: string): PublicKey =>
new PublicKey(getBytes(evmAddressToBytes32(addressHex)));
```
### Program Events
Program events like
[DepositForBurn](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L35-L45)
,
[MintAndWithdraw](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52)
, and
[MessageReceived](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/token-messenger-minter/src/token_messenger/events.rs#L47-L52)
are emitted as Anchor CPI events. This means a self-CPI is made into the program
with the serialized event as instruction data so it is persisted in the
transaction and can be fetched later on as needed. More information can be seen
in the
[Anchor implementation PR](https://github.com/coral-xyz/anchor/pull/2438), and
an example of reading CPI events can be seen in the
[solana-cctp-contracts repository](https://github.com/circlefin/solana-cctp-contracts/blob/master/tests/utils.ts#L62-L111).
[MessageSent](https://github.com/circlefin/solana-cctp-contracts/blob/master/programs/message-transmitter/src/events.rs#L49-L55)
events are different, as they are stored in accounts. Please see the
[MessageSent Event Storage section](#depositforburn) for more info.
# CCTP Sui Packages and Interfaces V1
Source: https://developers.circle.com/cctp/v1/sui-packages
Packages for CCTP V1 support on the Sui blockchain
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
## Overview
The CCTP V1 Sui smart contract implementation is written in
[Move](https://sui.io/move). The Sui CCTP V1 implementation is split into two
packages: `MessageTransmitter` and `TokenMessengerMinter`.
`TokenMessengerMinter` encapsulates the functionality of both `TokenMessenger`
and `TokenMinter` contracts on EVM chains. To ensure alignment with EVM
contracts logic and state, and to facilitate future upgrades and maintenance,
the code and state of the Sui packages reflect the EVM counterparts as closely
as possible.
There are a few key differences with Sui packages from EVM and other CCTP V1
implementations:
### Receive Message Flow
Since the Move language does not have interfaces, the
`message_transmitter::receive_message()` function cannot call directly into the
receiver package (e.g. `TokenMessenger` for USDC transfers). The workaround for
this limitation is that callers of `receive_message()` must also atomically (in
the same
[Programmable Transaction Block (PTB)](https://docs.sui.io/concepts/transactions/prog-txn-blocks))
call into the receiver package's `handle_receive_message()` function with a
`Receipt` struct, call `stamp_receipt()` with the `StampReceiptTicket` struct
returned from `handle_receive_message()`, and then pass the `StampedReceipt`
back into the `message_transmitter::complete_receive_message()` function to
complete the message and destroy the `Receipt` object. This flow ensures
atomicity and guarantees message receipt by the receiver packages. Please see
the interface and examples below for more information on this flow.
### Interacting with TokenMessengerMinter from other Packages
On Sui, when a package is upgraded, the new version is deployed with a new
package ID. This means if another package is directly calling a version-gated
function, when the package is upgraded, the dependent packages must also be
upgraded. To address this, all CCTP V1 functions that are intended to be called
from a dependent package follow a `Ticket` struct pattern. In this pattern, the
dependent package can call non version-gated `create_ticket()` functions with
the intended function parameters, including an `Auth` struct (used to uniquely
identify the package), and receive back a `Ticket` struct. This struct can then
be returned from the dependent package and used in a PTB to call the intended
CCTP V1 function. This allows integrators to securely integrate with CCTP V1
functions from their packages, and only have to update PTBs when CCTP V1
packages are upgraded rather than having to upgrade their packages as well. For
more information, see the functions below with the `_with_package_auth` suffix.
### Testnet
#### Package IDs
| Package | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | 8 | `0x4931e06dce648b3931f890035bd196920770e913e43e45990b383f6486fdd0a5` |
| TokenMessengerMinter | 8 | `0x31cc14d80c175ae39777c0238f20594c6d4869cfab199f40b69f3319956b8beb` |
#### Shared Object IDs
| Object | Object ID |
| :------------------------ | :------------------------------------------------------------------- |
| MessageTransmitterState | `0x98234bd0fa9ac12cc0a20a144a22e36d6a32f7e0a97baaeaf9c76cdc6d122d2e` |
| TokenMessengerMinterState | `0x5252abd1137094ed1db3e0d75bc36abcd287aee4bc310f8e047727ef5682e7c2` |
| USDC Treasury Object | `0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe` |
Branch with testnet
[Automated Address Management](https://docs.sui.io/concepts/sui-move-concepts/packages/automated-address-management):
[github.com/circlefin/sui-cctp/tree/testnet](https://github.com/circlefin/sui-cctp/tree/testnet).
### Mainnet
#### Package IDs
| Package | [Domain](/cctp/v1/supported-domains) | Address |
| :------------------- | :----------------------------------- | :------------------------------------------------------------------- |
| MessageTransmitter | 8 | `0x08d87d37ba49e785dde270a83f8e979605b03dc552b5548f26fdf2f49bf7ed1b` |
| TokenMessengerMinter | 8 | `0x2aa6c5d56376c371f88a6cc42e852824994993cb9bab8d3e6450cbe3cb32b94e` |
#### Shared Object IDs
| Object | Object ID |
| :------------------------ | :------------------------------------------------------------------- |
| MessageTransmitterState | `0xf68268c3d9b1df3215f2439400c1c4ea08ac4ef4bb7d6f3ca6a2a239e17510af` |
| TokenMessengerMinterState | `0x45993eecc0382f37419864992c12faee2238f5cfe22b98ad3bf455baf65c8a2f` |
| USDC Treasury Object | `0x57d6725e7a8b49a7b2a612f6bd66ab5f39fc95332ca48be421c3229d514a6de7` |
Branch with mainnet
[Automated Address Management](https://docs.sui.io/concepts/sui-move-concepts/packages/automated-address-management):
[github.com/circlefin/sui-cctp/tree/mainnet](https://github.com/circlefin/sui-cctp/tree/mainnet).
## Interface
The Sui CCTP V1 source code is
[available on GitHub](https://github.com/circlefin/sui-cctp/).
The interface below serves as a reference for permissionless messaging functions
exposed by the programs.
### TokenMessengerMinter
#### [deposit\_for\_burn](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L106)
Burns passed in tokens from sender to be minted on destination domain. Minted
tokens will be transferred to `mint_recipient` on the destination chain. The
`deposit_for_burn` interface and functionality is very similar to the EVM
implementation. The coins parameter is the key difference due to how passing
tokens around on Sui works. `message_transmitter_state`, `deny_list`, and
`treasury` parameters are all shared objects.
**Remarks:**
* Intended to be called directly by EOA (rather than a dependent package). The
initiating EOA will be the "owner" (e.g. message sender) of the message and
have the ability to call `replace_deposit_for_burn()` to update the
`mint_recipient` or `destination_caller`. If the calling EOA is not trusted by
the mint recipient or destination caller,
`deposit_for_burn_with_package_auth()` should be called instead with the
integrating package owning the message.
* The generic type T is the coin's one-time witness
([OTW](https://docs.sui.io/concepts/sui-move-concepts/one-time-witness)) type
for the specific coin type to be burned.
* `BurnMessage` and `Message` structs are returned, but it is not required to do
anything with these structs; they are returned for convenience.
**Parameters**
| Field | Type | Description |
| :-------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| coins | `Coin` | Coin of type T to be burned. Full amount in coins will be burned. |
| destination\_domain | `u32` | Destination domain identifier. |
| mint\_recipient | `address` | Address of mint recipient on destination domain. *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
| state | `&State` | Shared State object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. |
| deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` |
| treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [deposit\_for\_burn\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L143)
Same as `deposit_for_burn()`, but intended to be called with an `Auth` struct
from a dependent package. The calling package will be the "owner" (e.g.
message\_sender) of the message and have the ability to call
`replace_deposit_for_burn_with_package_auth()` to update the mint recipient or
destination caller. This would be similar to a wrapper contract on EVM chains
calling into `TokenMessenger` and being the message sender. Direct callers
(where EOAs are trusted and should be the owner) should use `deposit_for_burn()`
instead.
**Remarks:**
* This function uses a `DepositForBurnTicket` struct for parameters so that the
calling package can call `create_deposit_for_burn_ticket()` (not
version-gated) from their package with parameters, and call
`deposit_for_burn_with_package_auth()` (version-gated) from a PTB so packages
don't have to be updated during CCTP V1 package upgrades.
* `DepositForBurnTicket` also requires an `Auth` parameter. This is required to
securely assign a sender address associated with the calling contract to the
message. Any struct that implements the drop trait can be used as an
authenticator, but it is recommended to use a dedicated `Auth` struct. Calling
contracts should be careful to not expose these structs to the public or else
messages from their package could be replaced. An example can be found in
`TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
* The returned structs - `BurnMessage` and `Message` both have the copy ability.
There is also no guarantee of execution ordering, so your package could create
5 DepositForBurnTickets in one transaction and they could be executed in any
order depending on the PTB. Integrating packages should account for both of
these scenarios.
**Parameters**
| Field | Type | Description |
| :-------------------------- | :------------------------------ | :----------------------------------------------------------------------------- |
| deposit\_for\_burn\_ticket | `DepositForBurnTicket` | Struct containing parameters and authenticator struct. |
| state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. |
| deny\_list | `&DenyList` | DenyList shared object for the stablecoin token `T`. Constant address: `0x403` |
| treasury | `&mut Treasury` | Treasury shared object for the stablecoin token `T`. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [deposit\_for\_burn\_with\_caller](https://github.com/circlefin/sui-cctp/blob/649ed8a06840271ddc1ad66bb215d51be8265c31/packages/token_messenger_minter/sources/deposit_for_burn.move#L177)
Same as `deposit_for_burn` but with an additional parameter,
`destination_caller`. This parameter specifies which address has permission to
call `receive_message` on the destination domain for the message.
**Remarks:**
* Intended to be called directly by EOA (rather than a dependent package). The
initiating EOA will be the "owner" (e.g. message sender) of the message and
have the ability to call `replace_deposit_for_burn()` to update the
`mint_recipient` or `destination_caller`. If the calling EOA is not trusted by
the mint recipient or destination caller,
`deposit_for_burn_with_caller_with_package_auth()` should be called instead
with the integrating package owning the message.
**Destination Caller Notes**
If the `destination_caller` does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard `deposit_for_burn` should be preferred for
use cases where a specific destination caller is not required.
*Note: If destination is a non-Move chain,* `destination_caller` *address should
be converted to hex and passed in using the @0x123 address format.*
**Parameters**
| Field | Type | Description |
| :-------------------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| coins | `Coin` | Coin of type T to be burned. Full amount in coins will be burned. |
| destination\_domain | `u32` | Destination domain identifier. |
| mint\_recipient | `address` | Address of mint recipient on destination domain *Note: If destination is a non-Move chain,* `mint_recipient` *address should be converted to hex and passed in using the @0x123 address format.* |
| destination\_caller | `address` | Address of caller on destination chain. |
| state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. |
| deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` |
| treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [deposit\_for\_burn\_with\_caller\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L212)
The same as `deposit_for_burn_with_caller()`, but intended to be called with an
`Auth` struct from a dependent package. The calling package will be the "owner"
(e.g. message\_sender) of the message and have the ability to call
`replace_deposit_for_burn_with_package_auth()` to update the `mint_recipient` or
`destination_caller`. This would be similar to a wrapper contract on EVM chains
calling into `TokenMessenger` and being the message sender. Direct callers
(where EOAs are trusted and should be the owner) should use
`deposit_for_burn_with_caller()` instead.
**Remarks:**
* This function uses a `DepositForBurnWithCallerTicket` struct for parameters so
that the calling package can call
`create_deposit_for_burn_with_caller_ticket()` (not version-gated) from their
package, and call `deposit_for_burn_with_caller_with_package_auth()`
(version-gated) from a PTB so dependent packages don't have to be updated
during upgrades.
* `DepositForBurnWithCallerTicket` also requires an `Auth` parameter. This is
required to securely assign a sender address associated with the calling
contract to the message. Any struct that implements the drop trait can be used
as an authenticator, but it is recommended to use a dedicated struct. Calling
contracts should be careful to not expose these structs to the public or else
messages from their package could be replaced. An example can be found in
`TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
**Destination Caller Notes**
If the `destination_caller` does not represent a valid address, then it will not
be possible to broadcast the message on the destination domain. This is an
advanced feature, and the standard `deposit_for_burn` should be preferred for
use cases where a specific destination caller is not required.
*Note: If destination is a non-Move chain,* `destination_caller` *address should
be converted to hex and passed in using the @0x123 address format.*
**Parameters**
| Field | Type | Description |
| :--------------------------------------- | :---------------------------------------- | :--------------------------------------------------------------------------- |
| deposit\_for\_burn\_with\_caller\_ticket | `DepositForBurnWithCallerTicket` | Struct containing parameters and authenticator struct. |
| state | `&State` | Shared `State` object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared `State` object for the `MessageTransmitter` package. |
| deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` |
| treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [replace\_deposit\_for\_burn](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L259)
Replace a `BurnMessage` to change the mint recipient and/or destination caller.
Allows the sender of a previous `BurnMessage` (created by `deposit_for_burn` or
`deposit_for_burn_with_caller`) to send a new `BurnMessage` to replace the
original.
**Remarks:**
* The new `BurnMessage` will reuse the amount and burn token of the original,
without requiring a new `Coin` deposit.
* The resulting mint will supersede the original mint, as long as the original
mint has not confirmed yet onchain.
* A valid attestation is required before calling this function.
* This is useful in situations where the user specified an incorrect address and
has no way to safely mint the previously burned USDC.
**Parameters**
| Field | Type | Description |
| :-------------------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| original\_message | `vector` | Original message bytes (to replace). |
| original\_attestation | `vector` | Original attestation bytes. |
| new\_destination\_caller | `Option` | The new destination caller, which may be the same as the original destination caller, a new destination caller, or an empty destination caller, indicating that any destination caller is valid. |
| new\_mint\_recipient | `Option` | The new mint recipient, which may be the same as the original mint recipient, or different. |
| state | `&State` | Shared State object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [replace\_deposit\_for\_burn\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L282)
Same as `replace_deposit_for_burn()`, but intended to be called when
`deposit_for_burn_with_package_auth()` or
`deposit_for_burn_with_caller_with_package_auth()` was called for the original
message where the calling package is the message sender.
**Remarks:**
* This function uses a `ReplaceDepositForBurnTicket` struct for parameters so
that the calling package can call `create_replace_deposit_for_burn_ticket()`
(not version-gated) from their package with parameters, and call
`deposit_for_burn_with_package_auth()` (version-gated) from a PTB so packages
don't have to be updated during CCTP V1 package upgrades.
**Parameters**
| Field | Type | Description |
| :---------------------------------- | :---------------------------------- | :---------------------------------------------------------- |
| replace\_deposit\_for\_burn\_ticket | `ReplaceDepositForBurnTicket` | Struct containing parameters and authenticator struct. |
| state | `&State` | Shared State object for the `TokenMessengerMinter` package. |
| message\_transmitter\_state | `&mut MessageTransmitterState` | Shared State object for the `MessageTransmitter` package. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [handle\_receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/handle_receive_message.move#L131)
Handles an incoming message from `MessageTransmitter`, and mints USDC to the
recipient for valid messages. This function can only be called with a mutable
reference to a `Receipt` object, which can only be created via a call with a
valid message to the `message_transmitter::receive_message()` function.
`state`, `mt_state`, `deny_list`, and `treasury` parameters are all shared
objects.
**Remarks:**
* Returns a `StampReceiptTicketWithBurnMessage` struct that can be deconstructed
in a dependent package (or in a PTB) via
`deconstruct_stamp_receipt_ticket_with_burn_message()`. This struct is
returned so that dependent packages can associate the `BurnMessage` and
`StampReceiptTicket` together from a PTB call and guarantee that
`stamp_receipt()` was called.
* This must be called in a single PTB after calling `receive_message()` and
before calling `complete_receive_message()`. See the
[Examples](/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum) page for
the entire flow of receiving a message.
**Parameters**
| Field | Type | Description |
| :--------- | :----------------- | :--------------------------------------------------------------------------- |
| receipt | `Receipt` | Original message bytes (to replace). |
| state | `&State` | Shared State object for the `TokenMessengerMinter` package. |
| deny\_list | `&DenyList` | DenyList shared object for the stablecoin token T. Constant address: `0x403` |
| treasury | `&mut Treasury` | Treasury shared object for the stablecoin token T. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
### MessageTransmitter
#### [receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L113)
Receives a message emitted from a source chain. Messages with a given nonce can
only be received once for a (`sourceDomain`, `destinationDomain`) pair.
**Remarks:**
* This function returns a `Receipt`
[Hot Potato](https://medium.com/@borispovod/move-hot-potato-pattern-bbc48a48d93c)
struct after validating the attestation and marking the nonce as used.
* In order to destroy the `Receipt` and complete the message, in a single PTB,
`stamp_receipt()` must be called with the `Receipt` and an `Auth` struct (see
[message\_transmitter\_authenticator](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/deposit_for_burn.move#L212)
for an example of this), and then `complete_receive_message()` must be called
with the `StampedReceipt` to emit the `MessageReceived` event and complete the
message.
* The receipt/stamp pattern is used to enforce atomicity and ensure the intended
receiver contract is called.
* Intended to be called directly from an EOA when a package `destination_caller`
is not specified on the message. Please use
`receive_message_with_package_auth()` if a package `destination_caller` is
specified.
**Parameters**
| Field | Type | Description |
| :---------- | :----------- | :---------------------------------------------------------- |
| message | `vector` | Message bytes. |
| attestation | `vector` | Signed attestation of message. |
| state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [receive\_message\_with\_package\_auth](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L146)
Same as `receive_message()`, except intended to be used by a dependent package
when a package is specified as `destination_caller` (rather than an EOA).
**Remarks:**
* This function is version-gated and should be called from a PTB to prevent
breaking changes when an upgrade occurs.
* This function uses a `ReceiveMessageTicket` for parameters so that the calling
package can call `create_receive_message_ticket()` (not version-gated) from
their package with parameters, and call `receive_message_with_package_auth()`
(version-gated) from a PTB so packages don't have to be upgraded during CCTP
V1 package upgrades.
* `ReceiveMessageTicket` also requires an `Auth` parameter. This is required
whenever a package is assigned as a `destination_caller`. `destination_caller`
address should be set to the `Auth` identifier returned from the
`auth_caller_identifier()` function with the package's `Auth` struct. Any
struct that implements the drop trait can be used as an authenticator, but it
is recommended to use a dedicated `Auth` struct. Calling contracts should be
careful to not expose these structs to the public or else messages intended
for their package could be received by others. An example can be found in
`TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
**Parameters**
| Field | Type | Description |
| :----------------------- | :--------------------------- | :---------------------------------------------------------------------------------- |
| receive\_message\_ticket | `ReceiveMessageTicket` | A `Ticket` struct containing the message, attestation, and an authenticator struct. |
| state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [stamp\_receipt](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L164)
Stamps a `Receipt` struct after verifying the intended package acknowledged the
message (through the `Auth` struct) by returning a `StampedReceipt` struct that
can be used to complete the message via `complete_receive_message()`.
**Remarks:**
* This function is version-gated and should be called from a PTB to prevent
breaking changes in dependent packages when a CCTP V1 upgrade occurs.
* `create_stamp_receipt_ticket()` is safe to be called directly from a package
(not version-gated), and it's returned `Ticket` struct can be passed into
`stamp_receipt()` in a PTB.
**Auth Parameter Notes**
This is required for the `MessageTransmitter` module to approve a `Receipt`
prior to its deletion. Any struct that implements the drop trait can be used as
an authenticator, but it is recommended to use a dedicated `Auth` struct.
Calling contracts should be careful to not expose these `Auth` structs to the
public to avoid messages being wrongly stamped. An example implementation exists
in the `token_messenger_minter::message_transmitter_authenticator` module.
**Parameters**
| Field | Type | Description |
| :--------------------- | :------------------------- | :----------------------------------------------------------------------------------------------- |
| stamp\_receipt\_ticket | `StampReceiptTicket` | `Ticket` struct created by `create_stamp_receipt_ticket()` with the `Receipt` and `Auth` struct. |
| state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. |
| ctx | `&TxContext` | `TxContext` for the transaction. |
#### [complete\_receive\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/receive_message.move#L178)
Completes the message by emitting a `MessageReceived` event for a stamped
receipt and destroying the receipt. Cannot be called without a `StampedReceipt`
(returned from `stamp_receipt()`).
**Parameters**
| Field | Type | Description |
| :--------------- | :--------------- | :---------------------------------------------------------- |
| stamped\_receipt | `StampedReceipt` | A stamped receipt returned from a `stamp_receipt()` call. |
| state | `&State` | Shared State object for the `TokenMessengerMinter` package. |
#### [send\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L66)
Sends a message to the destination domain and recipient. The created `Message`
struct is returned, but it is not required to do anything with this struct, it
is returned for convenience.
**Remarks:**
* This function uses a `SendMessageTicket` for parameters so that the calling
package can call `create_send_message_ticket()` (not version-gated) from their
package with parameters, and call `send_message()` (version-gated) from a PTB
so packages don't have to be updated during CCTP V1 package upgrades.
* For USDC transfers, this function is called directly by the
`TokenMessengerMinter` package in `deposit_for_burn()`.
* `SendMessageTicket` also requires an `Auth` parameter. This is required in
order to assign a `sender` to the message. Any struct that implements the drop
trait can be used as an authenticator, but it is recommended to use a
dedicated `Auth` struct. Calling contracts should be careful to not expose
these objects to the public or else their messages could be replaced. An
example can be found in `TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
* The returned struct (`Message`) has the copy ability. There is also no
guarantee of execution ordering, so your package could create 5
`SendMessageTickets` in one transaction and they could be executed in any
order depending on the PTB. Integrating packages should account for both of
these scenarios.
**Parameters**
| Field | Type | Description |
| :-------------------- | :------------------------ | :---------------------------------------------------------------------------------------------------------- |
| send\_message\_ticket | `SendMessageTicket` | A struct containing the necessary information to send a message created via `create_send_message_ticket()`. |
| state | `&mut State` | Shared State object for the `TokenMessengerMinter` package. |
### [send\_message\_with\_caller](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L85)
Same as `send_message()` but with an additional parameter, `destination_caller`.
This parameter specifies which address has permission to call
`receive_message()` on the destination domain for the message.
**Parameters**
| Field | Type | Description |
| :---------------------------------- | :---------------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| send\_message\_with\_caller\_ticket | `SendMessageWithCallerTicket` | A struct containing the necessary information to send a message created via `create_send_message_with_caller_ticket()`. |
| state | `&mut State` | Shared `State` object for the `TokenMessengerMinter` package. |
### [replace\_message](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/message_transmitter/sources/send_message.move#L115https://github.com/circlefin/sui-cctp-private/blob/master/packages/message_transmitter/sources/send_message.move#L146)
Replace a message with a new message body and/or destination caller. The
`original_attestation` must be a valid attestation of `original_message`,
produced by Circle's attestation service.
**Remarks:**
* The sender package of the replaced message must be the same as the caller of
the original message. This is identified using the `Auth` generic parameter.
See [stamp\_receipt](#stamp_receipt) for more info on `Auth` structs.
**Parameters**
| Field | Type | Description |
| :----------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------- |
| replace\_message\_ticket | `ReplaceMessageTicket` | A struct containing the necessary information to send a message created via `create_replace_message_ticket()`. |
| state | `&mut State` | Shared `State` object for the `TokenMessengerMinter` package. |
## Additional Notes
### Destination Callers for Sui as Destination Chain
Destination caller is a message field that specifies which address has
permission to call `receive_message()` on the destination domain for the given
message. On Sui this can either be an EOA (use `receive_message()`) or an `Auth`
struct address for a package (use `receive_message_with_package_auth()`). Using
a package destination caller allows integrators to run any atomic action in the
same transaction that the message is received in.
In order to determine the address to use for the destination caller field for
Sui destination messages, please call
`message_transmitter::auth::auth_caller_identifier()` with your `Auth` struct
type.
In order to use a package destination caller with Sui destination messages,
integrators must create an `Auth` struct in their own package. Any struct that
implements the drop trait can be used as an authenticator, but it is recommended
to use a dedicated `Auth` struct. Integrators should be careful to not expose
these structs to the public or else messages with their package as destination
caller could be received by others. An example can be found in
`TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
### Mint Recipient Addresses for Sui as Source Chain
Outgoing mint recipient addresses from Sui are passed as Sui address types and
can be treated the same as a `bytes32` mint recipient parameter on EVM
implementations.
### Mint Recipient Addresses for Sui as Destination Chain
Sui mint recipient addresses from other chains should be treated the same as a
hex `bytes32` parameter.
### CCTP V1 Package Upgrades and Versioning
CCTP V1 packages on Sui are upgradable. Public functions like
`deposit_for_burn()`, `receive_message()`, etc. are version-gated. This means if
the CCTP V1 packages are upgraded, the old versions of these functions will no
longer be callable. Because of this, we do not recommend calling these functions
directly from packages, and instead recommend calling the create ticket
functions (not version-gated) directly from dependent packages, returning the
created `Ticket` from your package, and then calling the main public function
(e.g. `deposit_for_burn()` or `receive_message()`) from a PTB. By using the
create ticket functions, dependent packages can securely set the parameters and
`Auth` struct for the function call from within the package, and only have to
update PTBs when CCTP V1 packages are upgraded.
### Integrating with CCTP V1 Sui from other Packages
Integrating with the CCTP V1 Sui packages from other packages is different from
non-Sui implementations. Rather than directly wrapping the CCTP-Sui packages
like one would in Solidity, on Sui packages should interact with CCTP V1
packages in a more composable way. Third party packages should follow the
`Ticket` pattern with a dedicated and private `Auth` struct as described below.
#### Private Auth Structs
`Auth` structs are used throughout the CCTP V1 packages in functions intended to
be called from other dependent packages. The `auth_caller_identifier()` function
is used to uniquely identify other packages by hashing the full object type of
the type passed in. Any struct that implements the drop trait can be used as an
authenticator, but it is recommended to use a dedicated auth struct. Calling
contracts should be careful to not expose these structs to the public or else
messages from their package could be forged. An example can be found in
`TokenMessengerMinter`
[on GitHub](https://github.com/circlefin/sui-cctp/blob/004950f742a161b6acfe2331630233ac3de0f3ad/packages/token_messenger_minter/sources/message_transmitter_authenticator.move).
#### Ticket Pattern
The `Ticket` pattern is a pattern used in CCTP-Sui that enables the
composability of CCTP V1 with third-party packages. The pattern enables a
third-party integrator (package) to create a `Ticket` ("hot potato") for a
designated operation directly in their package without having to upgrade their
packages with future CCTP V1 upgrades. Only PTBs would need to be updated.
`Ticket` structs contain parameters for specific interactions with CCTP V1
packages. They can only be created from and consumed by the CCTP V1 packages in
a specific interaction, and do not have drop or store abilities, so must be used
in the PTB where they are created. They also contain an `Auth` field that should
only be created by the third-party package. The calling PTB should handle the
`Ticket` by calling the relevant CCTP V1 package, which will recognize the
third-party integrator as the action initiator.
The following public functions (designed for third-party integrators, EOAs
should use the entry versions) are implemented following the `Ticket` pattern.
Each of them creates or consumes their own specific `Ticket` type:
**`message_transmitter`:**
* `receive_message_with_package_auth()`
* `stamp_receipt()`
**`token_messenger_minter`:**
* `deposit_for_burn_with_package_auth()`
* `deposit_for_burn_with_caller_with_package_auth()`
* `replace_deposit_for_burn_with_package_auth()`
For example, a typical workflow in a PTB to replace a deposit by an integrator
would be:
1. The integrating package calls `create_replace_deposit_for_burn_ticket()` with
an `Auth` struct it defined, and returns this ticket.
2. The PTB calls `deposit_for_burn_with_caller_with_package_auth()` with the
ticket on behalf of the integrator.
3. `token_messenger_minter` will validate if the type hash of `Auth` matches the
original sender in the burn message.
#### PTB Function Call Ordering
Due to the composability of Sui and PTBs, along with the `Ticket` pattern, there
is no guarantee of ordering of calls within PTBs. The `Ticket` pattern
introduces behaviors similar to asynchronous functions in ordinary programming
contexts: when an integrator creates a ticket and returns it to the PTB, it is
signaling an intention to execute the logic function, and the properties of the
Move type system carry the guarantee that the function will indeed be eventually
executed before the end of the transaction. However, no guarantee is given
regarding the relative order of execution: the PTB is free to consume the
tickets in any order it sees fit. While this has no security implications on the
internal coherence of CCTP V1 itself, integrators should carefully evaluate
whether their own logic is somehow dependent on a specific order of execution of
the CCTP V1 functions.
For example, a PTB could create 5 `DepositForBurnTicket` structs and execute
them in any order. Similarly on the Sui destination side, 5 messages could be
received in `MessageTransmitter`, and then received (and thus the USDC minted)
in `TokenMessengerMinter` in a completely different order. If any pre or post
actions are taken in third party packages, these could also come in an
unexpected ordering, so this scenario should be handled accordingly in third
party packages.
#### Ticket Pattern Examples
An example of this with receiving `deposit_for_burn()` messages on Sui can be
seen below.
This example assumes the `destination_caller` for the message is set to the auth
address for your package's `Auth` struct.
```javascript JavaScript theme={null}
// Prepare the ReceiveMessageTicket by calling create_receive_message_ticket() from within your package.
let receive_msg_ticket = your_package::prepare_receive_message_ticket(message, attestation);
// Receive the message on MessageTransmitter.
let receipt = message_transmitter::receive_message_with_package_auth(receive_msg_ticket, ...);
// Pass the Receipt into TokenMessengerMinter to mint the USDC.
let ticket_with_burn_message = token_messenger_minter::handle_receive_message(receipt, ...);
// In your package you can call deconstruct_stamp_receipt_ticket_with_burn_message to deconstruct the ticket
// and burn_message and securely take some action with the burn_message (e.g. swap some tokens, send them somewhere, etc.)
let stamp_receipt_ticket = your_package::take_some_action(ticket_with_burn_message, ...);
// Stamp the receipt
let stamped_receipt = message_transmitter::stamp_receipt(stamp_receipt_ticket);
// Complete the message and destroy the StampedReceipt
message_transmitter::complete_receive_message(stamped_receipt);
```
A similar example can be seen on the `deposit_for_burn()` side:
```javascript JavaScript theme={null}
// Prepare the DepositForBurnWithCallerTicket by calling create_deposit_for_burn_with_caller_with_package_auth
// directly from your package with the input parameters and your Auth struct. Integrators can also take other
// actions here as needed.
let deposit_for_burn_ticket = your_package::prepare_deposit_for_burn_ticket(coins, ...);
// Call deposit for burn and burn the USDC
let (burn_message, message) = token_messenger_minter::deposit_for_burn_with_caller_with_package_auth(
deposit_for_burn_ticket,
...
);
// Optionally, take some other action in your package based on the output message.
// Note that BurnMessage and Message have the copy ability so the possibility of them being copied should be
// handled in third party packages if post-actions are needed.
your_package::post_deposit_for_burn(burn_message, message, ...);
```
# CCTP Chain Domains V1
Source: https://developers.circle.com/cctp/v1/supported-domains
Mapping of supported CCTP V1 domains
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
A **domain** is a Circle-issued identifier for a blockchain where CCTP V1
contracts are deployed. Domains do not map to any existing public chain ID.
## CCTP V1 Domains
| Domain | Name |
| :----- | :---------- |
| 0 | Ethereum |
| 1 | Avalanche |
| 2 | OP |
| 3 | Arbitrum |
| 4 | Noble |
| 5 | Solana |
| 6 | Base |
| 7 | Polygon PoS |
| 8 | Sui |
| 9 | Aptos |
| 10 | Unichain |
# Transfer USDC on devnet between Solana and other chains using CCTP V1
Source: https://developers.circle.com/cctp/v1/transfer-testnet-usdc-between-solana-devnet
Explore this tutorial for transferring USDC between Solana devnet and other testnets via CCTP V1
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
To get started with CCTP V1 on Solana devnet, follow the
[example scripts](https://github.com/circlefin/solana-cctp-contracts/tree/master/examples).
The examples use [Solana web3.js](https://www.npmjs.com/package/@solana/web3.js)
and [Anchor](https://www.npmjs.com/package/@project-serum/anchor) to transfer
USDC to and from an account on Solana devnet and an address on an external
blockchain.
As a security measure, these scripts should only be used for devnet testing. You
should not reuse private keys across devnet and mainnet.
# Transfer USDC on testnet between Aptos and Base using CCTP V1
Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-aptos-to-base
Explore this tutorial for transferring USDC between Aptos testnet and Base Sepolia Testnet via CCTP V1
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
To get started with CCTP V1 on Aptos testnet, follow the example scripts
provided
[on GitHub](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example).
The examples use the
[Aptos SDK](https://www.npmjs.com/package/@aptos-labs/ts-sdk), to transfer USDC
to and from an address on Aptos testnet and an address on an external
blockchain.
**Do not reuse keys** As a security measure, these scripts should only be used
on a testnet for testing purposes. It is not recommended to reuse private keys
across mainnet and testnet.
Summary of calling `deposit_for_burn()` (full runnable script can be found in
the aptos-cctp repository):
```ts theme={null}
// Aptos Testnet Stablecoin object
const BURN_TOKEN =
"0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832";
const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const userAccount = Account.fromPrivateKey({
privateKey: new Ed25519PrivateKey(APTOS_PRIVATE_KEY),
});
// Create a transaction with deposit for burn script
const buffer = readFileSync(
"typescript/example/precompiled-move-scripts/testnet/deposit_for_burn.mv",
);
const bytecode = Uint8Array.from(buffer);
const amount = new U64(1);
const destinationDomain = new U32(6);
const burnToken = AccountAddress.from(BURN_TOKEN);
const mintRecipient = AccountAddress.from(evmSigner.address);
const functionArguments: Array = [
amount,
destinationDomain,
mintRecipient,
burnToken,
];
const transaction = await aptosClient.transaction.build.simple({
sender: userAccount.accountAddress,
data: {
bytecode,
functionArguments,
},
});
const pendingTxn = await aptosClient.signAndSubmitTransaction({
signer: userAccount,
transaction,
});
const depositForBurnTx = await aptosClient.waitForTransaction({
transactionHash: pendingTxn.hash,
});
console.log(
`Deposit for burn transaction completed successfully: https://explorer.aptoslabs.com/txn/${depositForBurnTx.hash}`,
);
// Fetch the event data from the transaction
const messageSentEvent = (
depositForBurnTx as UserTransactionResponse
).events.find(
(e: any) =>
e.type ===
`${MESSAGE_TRANSMITTER_PACKAGE_ID}::message_transmitter::MessageSent`,
);
```
Summary of calling `receive_message()` (full runnable script can be found in the
aptos-cctp repository):
```ts theme={null}
const bytecode = Uint8Array.from(
fs.readFileSync(
"typescript/example/precompiled-move-scripts/testnet/handle_receive_message.mv",
),
);
const functionArguments: Array = [
MoveVector.U8(messageBytes as Buffer),
MoveVector.U8(attestationSignature),
];
const transaction = await aptosClient.transaction.build.simple({
sender: userAccount.accountAddress,
data: {
bytecode,
functionArguments,
},
});
const pendingTxn = await aptosClient.signAndSubmitTransaction({
signer: userAccount,
transaction,
});
const receiveMessageTx = await aptosClient.waitForTransaction({
transactionHash: pendingTxn.hash,
});
console.log(
`Receive message transaction completed successfully: https://explorer.aptoslabs.com/txn/${receiveMessageTx.hash}`,
);
```
# Transfer USDC on testnet from Ethereum to Avalanche using CCTP V1
Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-ethereum-to-avalanche
Explore this script to transfer USDC on testnet between two EVM-compatible chains via CCTP V1
**This is CCTP V1 version. For the latest version, see [CCTP](/cctp)**.
This guide demonstrates how to use the [viem](https://viem.sh/) framework and
the [CCTP V1 API](/cctp/v1/cctp-apis) in a simple script that enables a user to
transfer USDC from a wallet address on the **Ethereum Sepolia testnet** to
another wallet address on the **Avalanche Fuji testnet**.
To get started with CCTP V1, follow the example script provided
[on GitHub](https://github.com/circlefin/evm-cctp-contracts/blob/d1c24577fb627b08483dc42e4d8a37a810b369f7/docs/index.js).
The example uses
[web3.js](https://web3js.readthedocs.io/en/v1.8.1/getting-started.html) to
transfer USDC from a wallet address on Ethereum Sepolia testnet to another
wallet address on Avalanche Fuji testnet.
The script has five steps:
1. In this first step, you initiate a transfer of USDC from one blockchain to
another, and specify the recipient wallet address on the destination chain.
This step approves the Ethereum Sepolia **TokenMessenger** contract to
withdraw USDC from the provided Ethereum Sepolia wallet address.
```javascript JavaScript theme={null}
const approveTx = await usdcEthContract.methods
.approve(ETH_TOKEN_MESSENGER_CONTRACT_ADDRESS, amount)
.send({ gas: approveTxGas });
```
2. In this second step, you facilitate a burn of the specified amount of USDC on
the source chain. This step executes the `depositForBurn` function on the
Ethereum Sepolia **TokenMessenger** contract deployed on
[Sepolia testnet](https://sepolia.etherscan.io/address/0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5).
```javascript JavaScript theme={null}
const burnTx = await ethTokenMessengerContract.methods
.depositForBurn(
amount,
AVAX_DESTINATION_DOMAIN,
destinationAddressInBytes32,
USDC_ETH_CONTRACT_ADDRESS,
)
.send();
```
3. In this third step, you make sure you have the correct message and hash it.
This step extracts `messageBytes` emitted by the **MessageSent** event from
`depositForBurn` transaction logs and hashes the retrieved `messageBytes`
using the **keccak256** hashing algorithm.
```javascript JavaScript theme={null}
const transactionReceipt = await web3.eth.getTransactionReceipt(
burnTx.transactionHash,
);
const eventTopic = web3.utils.keccak256("MessageSent(bytes)");
const log = transactionReceipt.logs.find((l) => l.topics[0] === eventTopic);
const messageBytes = web3.eth.abi.decodeParameters(["bytes"], log.data)[0];
const messageHash = web3.utils.keccak256(messageBytes);
```
4. In this fourth step, you request the attestation from Circle, which provides
authorization to mint the specified amount of USDC on the destination chain.
This step polls the attestation service to acquire the signature using the
`messageHash` from the previous step.
**Rate Limit**
The attestation service rate limit is 35 requests per second. If you exceed 35
requests per second, the service blocks all API requests for the next 5 minutes
and returns an HTTP 429 response.
```javascript JavaScript theme={null}
let attestationResponse = { status: "pending" };
while (attestationResponse.status != "complete") {
const response = await fetch(
`https://iris-api-sandbox.circle.com/attestations/${messageHash}`,
);
attestationResponse = await response.json();
await new Promise((r) => setTimeout(r, 2000));
}
```
5. In this final step, you enable USDC to be minted on the destination chain.
This step calls the `receiveMessage` function on the Avalanche Fuji
**MessageTransmitter** contract to receive USDC at the Avalanche Fuji wallet
address.
```javascript JavaScript theme={null}
const receiveTx = await avaxMessageTransmitterContract.receiveMessage(
receivingMessageBytes,
signature,
);
```
You have successfully transferred USDC between two EVM-compatible chains using
CCTP V1 end-to-end.
# Transfer USDC on testnet between Noble and Ethereum using CCTP V1
Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-noble-to-ethereum
Explore this tutorial for transferring USDC on testnet between Noble via CCTP V1 and Ethereum
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
To transfer USDC between Noble testnet and Ethereum Sepolia, follow the
tutorials provided
[on GitHub](https://github.com/circlefin/noble-cctp/tree/master/examples).
Specifically, follow the instructions for the
[DepositForBurn script](https://github.com/circlefin/noble-cctp/blob/master/examples/depositForBurn.ts)
to test transferring USDC from Noble testnet to Ethereum Sepolia, and the
instructions for the
[ReceiveMessage script](https://github.com/circlefin/noble-cctp/blob/master/examples/receiveMessage.ts)
to transfer USDC from Ethereum Sepolia to Noble testnet.
As a security measure, these scripts should only be used for testnet testing. It
is not recommended to reuse private keys across mainnet and testnet.
**Note:**
This tutorial relies on the Strangelove Ventures
[Noble CCTP V1 relayer](https://github.com/strangelove-ventures/noble-cctp-relayer),
which is a service that automatically calls `receiveMessage()` for messages
transmitted to and from Noble domains. To avoid relying on this relayer, you can
submit this `receiveMessage()` transaction directly. (If you do not want your
transaction relayed automatically, you can specify a `destinationCaller` via
`depositForBurnWithCaller()`.)
# Transfer USDC on testnet between Sui and Ethereum using CCTP V1
Source: https://developers.circle.com/cctp/v1/transfer-usdc-on-testnet-from-sui-to-ethereum
Explore this tutorial for transferring USDC between Sui testnet and Ethereum Sepolia Testnet via CCTP V1
**This is CCTP V1 (Legacy) version. For the latest version, see [CCTP](/cctp)**.
To get started with CCTP V1 on Sui testnet, follow the example scripts provided
[on GitHub](https://github.com/circlefin/sui-cctp/tree/master/scripts/sui-scripts).
The
[README](https://github.com/circlefin/sui-cctp?tab=readme-ov-file#run-localnet-example-scripts)
contains instructions for running the scripts. The examples use the
[Sui SDK](https://www.npmjs.com/package/@mysten/sui), to transfer USDC to and
from an address on Sui testnet and an address on an external blockchain.
**Do not reuse keys** As a security measure, these scripts should only be used
on a testnet for testing purposes. It is not recommended to reuse private keys
across mainnet and testnet.
Summary of calling `deposit_for_burn()` (full runnable script can be found in
the sui-cctp repository):
```javascript JavaScript theme={null}
// Create DepositForBurn tx
const depositForBurnTx = new Transaction();
// Split USDC to send in depositForBurn call
const ownedCoins = await client.getAllCoins({owner: signer.toSuiAddress()})
const usdcStruct = ownedCoins.data.find(c => c.coinType.includes(usdcId));
if (!usdcStruct || Number(usdcStruct.balance) < USDC_AMOUNT) {
throw new Error("Insufficient tokens in wallet to initiate transfer.");
}
const [coin] = depositForBurnTx.splitCoins(
usdcStruct.coinObjectId,
[USDC_AMOUNT]
);
// Create the deposit_for_burn move call
depositForBurnTx.moveCall({
target: `${tokenMessengerMinterId}::deposit_for_burn::deposit_for_burn`,
arguments: [
depositForBurnTx.object(coin), // Coin
depositForBurnTx.pure.u32(DESTINATION_DOMAIN), // destination_domain
depositForBurnTx.pure.address(evmUserAddress), // mint_recipient
depositForBurnTx.object(tokenMessengerMinterStateId), // token_messenger_minter state
depositForBurnTx.object(messageTransmitterStateId), // message_transmitter state
depositForBurnTx.object("0x403"), // deny_list id, fixed address
depositForBurnTx.object(treasuryId) // treasury object Treasury
],
typeArguments: [`${usdcId}::usdc::USDC`],
});
// Broadcast the transaction
console.log("Broadcasting sui deposit_for_burn tx...");
const depositForBurnOutput = await executeTransactionHelper({
client: client,
signer: signer,
transaction: depositForBurnTx,
});
assert(!depositForBurnOutput.errors);
console.log(`deposit_for_burn transaction successful: 0x${depositForBurnOutput.digest} \n`);
// Get USDC balance changes (optional)
const suiUsdcBalanceChange = depositForBurnOutput.balanceChanges?.find(b => b.coinType.includes(usdcId))
const balances = await client.getAllBalances({ owner: signer.toSuiAddress() });
const usdcBalance = balances.find(b => b.coinType.includes(usdcId))?.totalBalance;
// Get the message emitted from the tx
const messageRaw: Uint8Array = (depositForBurnOutput.events?.find((event) =>
event.type.includes("send_message::MessageSent")
)?.parsedJson as any).message;
const messageBuffer = Buffer.from(messageRaw);
const messageHex = `0x${messageBuffer.toString("hex")}`;
const messageHash = web3.utils.keccak256(messageHex);
console.log(`Message hash: ${messageHash}`);
```
Summary of calling `receive_message()` (full runnable script can be found in the
sui-cctp repository):
```javascript JavaScript theme={null}
// Create receiveMessage transaction
const receiveMessageTx = new Transaction();
// Add receive_message move call to MessageTransmitter
const [receipt] = receiveMessageTx.moveCall({
target: `${messageTransmitterId}::receive_message::receive_message`,
arguments: [
receiveMessageTx.pure.vector(
"u8",
Buffer.from(evmBurnTx.message.replace("0x", ""), "hex"),
), // message as byte array
receiveMessageTx.pure.vector(
"u8",
Buffer.from(attestation.replace("0x", ""), "hex"),
), // attestation as byte array
receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
],
});
// Add handle_receive_message call to TokenMessengerMinter with Receipt from receive_message call
const [stampReceiptTicketWithBurnMessage] = receiveMessageTx.moveCall({
target: `${tokenMessengerMinterId}::handle_receive_message::handle_receive_message`,
arguments: [
receipt, // Receipt object returned from receive_message call
receiveMessageTx.object(tokenMessengerMinterStateId), // token_messenger_minter state
receiveMessageTx.object("0x403"), // deny list, fixed address
receiveMessageTx.object(treasuryId), // usdc treasury object Treasury
],
typeArguments: [`${usdcId}::usdc::USDC`],
});
// Add deconstruct_stamp_receipt_ticket_with_burn_message call
const [stampReceiptTicket] = receiveMessageTx.moveCall({
target: `${tokenMessengerMinterId}::handle_receive_message::deconstruct_stamp_receipt_ticket_with_burn_message`,
arguments: [stampReceiptTicketWithBurnMessage],
});
// Add stamp_receipt call
const [stampedReceipt] = receiveMessageTx.moveCall({
target: `${messageTransmitterId}::receive_message::stamp_receipt`,
arguments: [
stampReceiptTicket, // Receipt ticket returned from deconstruct_stamp_receipt_ticket_with_burn_message call
receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
],
typeArguments: [
`${tokenMessengerMinterId}::message_transmitter_authenticator::MessageTransmitterAuthenticator`,
],
});
// Add complete_receive_message call to MessageTransmitter with StampedReceipt from stamp_receipt call.
// Receipt and StampedReceipt are Hot Potatoes so they must be destroyed for the
// transaction to succeed.
receiveMessageTx.moveCall({
target: `${messageTransmitterId}::receive_message::complete_receive_message`,
arguments: [
stampedReceipt, // Stamped receipt object returned from handle_receive_message call
receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
],
});
// Broadcast the transaction
console.log("Broadcasting Sui receive_message tx...");
const receiveMessageOutput = await executeTransactionHelper({
client: client,
signer: signer,
transaction: receiveMessageTx,
});
```
# Circle Mint
Source: https://developers.circle.com/circle-mint
Mint and redeem USDC and EURC directly from Circle.
Circle Mint is for institutional customers minting USDC or EURC. It is
typically used by exchanges, institutional traders, wallet providers, banks,
and consumer-app companies. [Contact
Circle](https://www.circle.com/mint-contact) to learn more.
Circle Mint lets you mint (convert fiat to stablecoins) and redeem (convert
stablecoins back to fiat) [USDC](/stablecoins/what-is-usdc) and EURC directly
from Circle, the sole issuer of both stablecoins. Every token redeems 1:1 for
its backing fiat currency: U.S. dollars for USDC, euros for EURC.
Manage your account through the [Mint Console](https://app.circle.com/) or the
Circle Mint API. Deposit fiat from a linked bank account, convert it to USDC or
EURC, and send stablecoins to blockchain wallets globally. To start integrating,
[set up your account and API key](/circle-mint/quickstarts/getting-started).
## What you can do
Convert fiat to USDC or EURC and redeem stablecoins back to fiat through
your Circle Mint account.
Send and receive USDC and EURC on supported blockchains.
Use Circle's Payment APIs to accept USDC deposits from your customers.
Convert between local currencies and USDC using cross-currency APIs.
# Additional APIs
Source: https://developers.circle.com/circle-mint/additional-apis
Circle Mint customers can access extra APIs to use USDC in their apps.
Circle Mint customers can access extra APIs to use USDC in their apps.
These APIs cost extra. Your use case might need more support from Circle.
Contact your Circle representative or
[sales@circle.com](mailto:sales@circle.com) for more information.
## Move money onchain
Use the Crypto Deposits and Crypto Payouts APIs to manage your Mint account.
These APIs let you build custom software that performs Circle Mint actions.
* Accept stablecoin payins: Take USDC and EURC on
[supported blockchains](/circle-mint/references/supported-chains-and-currencies#crypto-deposits-api-and-crypto-payouts-api)
through payment intents.
* Make stablecoin payouts: Send fast, low-cost onchain payouts to customers,
vendors, and suppliers.
### Stablecoin payins
With the Crypto Deposits API, you take USDC and EURC through onchain transfers
into your Mint wallet using payment intents. For details, see
[How Stablecoin Payins and Payouts Work](/circle-mint/how-stablecoin-payins-and-payouts-work).
Read the quickstart to
[receive a stablecoin payin](/circle-mint/receive-stablecoin-payin).
### Stablecoin payouts
The Crypto Payouts API sends payouts to customers, vendors, or suppliers on
supported blockchains.
You can fund payouts in USDC with your Circle Mint account. Your account can
receive deposits from traditional and blockchain payment rails.
Read the quickstart to
[send stablecoin payouts](/circle-mint/send-stablecoin-payout).
## Currency exchange
The Currency Exchange API lets you swap local currency for USDC and USDC for
EURC.
[Read the quickstart guide on the Cross-Currency API](/circle-mint/exchange-local-currency-usdc).
## Credit API
The Credit API provides programmatic access to Circle Mint's credit products:
Settlement Advance and Line of Credit. You can initiate draws, track transfer
status, manage fees, and process repayments through a dedicated set of endpoints
under `/v1/credit/`.
* **Settlement Advance**: Reserve funds, upload wire proof, and receive
disbursement after Treasury approval.
* **Line of Credit**: Request draws with auto-approval and repay outstanding
balances using USDC from your Circle Mint wallet.
[Read the Credit API overview](/circle-mint/credit-api/overview).
# API Logs
Source: https://developers.circle.com/circle-mint/api-logs
View API log entries in the Developer Tab to review transactions and debug any errors.
*[API Logs](https://app-sandbox.circle.com/developer/logs) are transaction logs
that enable you to view your API transaction history and debug API errors with
no setup required. They show the most accurate representation of what Circle
received and sent. When you send an API request, Circle stores it along with its
associated response. On the API logs page, you can view these API logs for seven
days after the request is sent and filter them to find the specific request you
are looking for. You can use the API logs to debug API errors without having to
log out of your system.*
* To access the logs, go to [Circle Sandbox](https://app-sandbox.circle.com) or
[Circle Mint](https://app.circle.com) and click the **Developer Tab > Logs**.
## API Log Data Elements
For each API request and response, Circle stores the following information,
which you can retrieve and review in the Developer tab.
| Field | Description |
| :------------ | :---------------------------------------------------------------------------------------------------------------------- |
| HTTP Status | HTTP Status code for each request such as 200 or 400. |
| Path | The path excludes the base URL. |
| Request ID | The `X-Request-Id` in the header provided in the request or is generated plus by Circle and returned in the response. |
| User Agent | The User-Agent field in the request header. It is common that the HTTP library used will provide this field by default. |
| Idempotency | Idempotency Key sent in the request body. This is only found in POST requests. |
| Origin | Includes the protocol (HTTP/HTTPS), the domain or IP address, and the port number if applicable. |
| Time | Timestamp of when the request was received. |
| Request Body | The full request body. |
| Response Body | The full response body. |
## API Log Filtering
To filter your search, use the search fields and popup menus at the top of the
Circle Sandbox or Circle Mint page.
| Filter Name | Description |
| :---------- | :------------------------------------------------------------------------------------------------------------- |
| Search | Filter by request ID, resource ID, or idempotency key ID. |
| Date Range | Filter results by date range. |
| Status | Filter results by `succeeded` and/or `failed`. Succeeded includes all 2## and failed includes all 4## and 5##. |
| Method | Filter results by HTTP Method. Supports `POST`, `PUT`, `PATCH`, `DELETE`, and/or `GET`. |
| Path | Filter results by the URL path such as `/payments`. |
## Data Redaction
Due to the sensitivity of some data provided to Circle, including financial
payment methods and PII data, some values will be redacted from the request and
response payloads. Values that are redacted are replaced with the value
`“[redacted].”` If you need access to this sensitive data, reach out to your
Circle customer success manager.
# CAMT.053 Daily Statements for Reconciliation
Source: https://developers.circle.com/circle-mint/camt053-statements
Integrate ISO 20022 CAMT.053 daily statement XML from Circle Mint for treasury and reconciliation workflows.
[ISO 20022](https://www.iso20022.org/) CAMT.053 is a standard bank-to-customer
statement format used for treasury reconciliation. Circle Mint generates one
CAMT.053.001.13 XML file per calendar day (UTC), covering all USDC and EURC
activity on your account. You
[retrieve the file](/circle-mint/camt053-statements#retrieve-the-report) through
the Circle Mint API and parse it to reconcile balances, match transactions, and
integrate with your treasury systems.
## Availability and SLA
* **Coverage**: Each report covers the prior calendar day from 00:00 through
24:00 UTC.
* **When it is ready**: The report is committed to be available by 06:00 UTC the
next day. Typically, it is ready around 02:00 UTC.
* **One file, multiple currencies**: USDC and EURC statements are delivered in a
single XML file as separate `Stmt` blocks.
Subscribe to the [Circle status page](https://status.circle.com/) to receive
updates on CAMT.053 outages.
## Retrieve the report
Retrieve the CAMT.053 report by first calling
[Request a report](/api-reference/circle-mint/account/request-report) with
`reportType: camt053` and `date` in `YYYY-MM-DD` format (the UTC calendar date
the statement covers), then use the returned `id` with one of the following
endpoints:
* [Get report by ID](/api-reference/circle-mint/account/get-report-by-id):
Returns JSON with a `data` object that includes `downloadUrl` and `expiresAt`.
Download the XML from `downloadUrl` before the time indicated by `expiresAt`.
* [Get report content](/api-reference/circle-mint/account/get-report-content):
Returns the raw XML as `application/xml`. The response uses a
`Content-Disposition` attachment filename such as
`camt053_YYYY-MM-DD_report.xml`.
## Read and reconcile the report
The report follows the ISO 20022 CAMT.053.001.13 structure. The sections below
cover the key elements you need for reconciliation.
### File format at a glance
The report uses ISO 20022 CAMT.053.001.13 XML structure. See the
[ISO 20022 message definitions catalog](https://www.iso20022.org/iso-20022-message-definitions)
for official message definitions.
* The file uses namespace `urn:iso:std:iso:20022:tech:xsd:camt.053.001.13`.
* The root is `Document` → `BkToCstmrStmt` → one or more `Stmt` elements.
* Each `Stmt` is one account and currency.
* Inside a statement you will see opening and closing balances and `Ntry`
(entry) elements. There is one `Ntry` per movement that affects the balance.
### Balances
Each `Stmt` includes opening and closing balances for the report date:
* `OPBD`: Opening booked balance for the report date.
* `CLBD`: Closing booked balance for the report date.
Each balance has an amount with `Ccy` (currency), `CdtDbtInd` (credit or debit
side of the balance), and a timestamp. For reconciliation, a common check is:
opening balance plus the sum of entries (respecting credit and debit) aligns
with closing balance for that statement.
### Currency vs token
Standard ISO 20022 fields follow ISO 4217. They use three-letter currency codes
on `Amt` and related standard elements:
* `USD` for USDC transactions (for example ``).
* `EUR` for EURC transactions (for example ``).
The full token identifier (`USDC` or `EURC`) is preserved on each transaction
line. At the transaction level it follows the path `Ntry` → `NtryDtls` →
`TxDtls` → `SplmtryData` → [`CircleTxn`](#circle-transaction-details-circletxn)
→ `Token`.
Use both when you need to match fiat-style accounting codes and token
identifiers.
### Transaction entries (``)
Each `Ntry` is one transaction line. Important children include:
* `Amt`: Amount and ISO currency on the amount (`USD` or `EUR`).
* `CdtDbtInd`: `CRDT` (credit) or `DBIT` (debit).
* `Sts`: Booking status in `Cd`, such as:
* `BOOK`: Booked
* `PDNG`: Pending
* `RJCT`: Rejected
* `FAIL`: Failed
* `BookgDt`: Booking time in `DtTm`, UTC (ISO 8601).
* `BkTxCd`: Circle's label for the transaction type, found under `Prtry` / `Cd`.
New labels can appear over time. Unmapped types may show as `UNKNOWN`.
### Circle transaction details (`CircleTxn`)
Circle adds detail under `Ntry` → `NtryDtls` → `TxDtls` → `SplmtryData` where
`PlcAndNm` is `CircleTransactionData`. Inside the envelope, `CircleTxn` uses
namespace `urn:circle:camt053:transaction`.
The following child elements may appear on a transaction line. This list is not
exhaustive — your parser should tolerate additional elements and fields that are
omitted.
* `TransactionId`: Identifier for the movement.
* `JobId`: Related job identifier.
* `Token`: Full token identifier (`USDC` or `EURC`).
* `CustomReferenceId`: Customer-provided reference when supplied.
* `ExternalReferenceId`: EFT-style reference when supplied (for example `IMAD`
or `UETR`).
* `Blockchain`: Blockchain identifier when the movement is onchain.
* `TransactionHash`: Onchain transaction hash when present.
* `Source`: Originating party or address when populated.
* `SourceType`: Type of source (for example fiat account or blockchain address)
when populated.
* `Destination`: Receiving party or address when populated.
* `DestinationType`: Type of destination when populated.
* `CustomerId`: Customer association when present.
Use these fields to tie a line back to APIs, onchain activity, or internal
references.
## Example truncated statement
The example below shows the header, one statement with opening and closing
balances, and one entry (abbreviated). It is only for orientation. Your real
files can contain many entries and a second statement for the other currency.
```xml Example CAMT.053 fragment theme={null}
CAMT053_0455_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
```
# API Resource Data Models
Source: https://developers.circle.com/circle-mint/circle-api-resources
Discover the many resources used by Circle APIs to enable payouts, payments, and transfers.
Circle APIs are a set of functions and procedures that allow you to use USDC for
payments, payouts, and transfers. Circle's APIs are built around several
resources that represent payments, payouts, transfers, and their associated
objects.
## Primary resources
### Payout object
A `payout` object represents a payout to a customer, vendor, or supplier.
#### Example
```json JSON theme={null}
{
"id": "df3b8e5f-9579-4c1f-9fa9-deac7f4be55c",
"sourceWalletId": "1000038499",
"destination": {
"id": "4d260293-d17c-4309-a8da-fa7850f95c10",
"type": "address_book",
},
"amount": {
"amount": "10.0",
"currency": "USD"
},
"fees": {
"amount": 3.0,
"currency": "USD"
},
"status": "complete",
"errorCode": "insufficient_funds",
"riskEvaluation": {
"decision": "denied",
"reason": "3000"
},
"return": { ... },
"createDate": "2020-12-24T11:19:20.561Z",
"updateDate": "2020-12-24T12:01:00.523Z",
}
```
#### Attributes
***
**`id`** *string* A `UUID` for the payout.
***
**`sourceWalletId`** *string* The identifier of the source wallet used to fund a
payout.
***
**`destination`** *object* The
[Destination object](#source-and-destination-objects) the payout is being made
to.
***
**`amount`** *object* A [Money object](#money-object) representing the total
amount that is paid to the destination.
***
**`toAmount`** *object* A [Money object](#money-object) representing the amount
that is paid to the destination currency. Only included for stablecoin payouts.
***
**`fees`** *object* A [Money object](#money-object) representing fees associated
with this payment.
***
**`status`** *string* Status of the payout. `pending` indicates that the payout
is in process; `complete` indicates it finished successfully; `failed` indicates
it failed.
***
**`externalRef`** *string* External network identifier which will be present
once provided from the applicable network.
***
**`errorCode`** *string* Indicates the failure reason of a payout. Only present
for payouts in failed state. Possible values are `insufficient_funds`,
`transaction_denied`, `transaction_failed`, and `transaction_returned`.
***
**`riskEvaluation`** *object* An object with two attributes, `decision` and
`reason`.
***
**`return`** *object* A Return object if the payout was returned.
***
**`createDate`** *string* ISO-8601 UTC date/time format.
***
**`updateDate`** *string* ISO-8601 UTC date/time format.
### Transfer object
A `transfer` object represents a transfer of funds from a Circle wallet to a
blockchain address, from a blockchain address to a Circle wallet, or between two
Circle wallets.
#### Example
```json JSON theme={null}
{
"id": "0d46b642-3a5f-4071-a747-4053b7df2f99",
"source": {
"type": "blockchain",
"chain": "ETH",
"address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F"
},
"destination": {
"type": "wallet",
"id": "12345"
},
"amount": {
"amount": "3.14",
"currency": "USD"
},
"transactionHash": "0x4cebf8f90c9243a23c77e4ae20df691469e4b933b295a73376292843968f7a63",
"status": "pending",
"riskEvaluation": {
"decision": "approved",
"reason": "1234"
},
"createDate": "2020-04-10T02:29:53.888Z"
}
```
#### Attributes
***
**`id`** *string* A `UUID` for the transfer.
***
**`source`** *object* A [Source object](#source-and-destination-objects)
representing the source of the transfer.
***
**`destination`** *object* A
[Destination object](#source-and-destination-objects) representing the
destination of the transfer.
***
**`amount`** *object* A [Money object](#money-object) representing the amount
transferred between source and destination.
***
**`transactionHash`** *string* A hash that uniquely identifies an onchain
transaction. This is only available when either `source` or `destination` are of
type `blockchain`.
***
**`status`** *string* Status of the transfer. Status `pending` indicates that
the transfer is in the process of running; `complete` indicates it finished
successfully; `failed` indicates it failed.
***
**`createDate`** *string* ISO-8601 UTC date/time format.
***
## Nested resources
The following are resources that are commonly used in other objects.
### Source and destination objects
Payments, payouts, and transfers reference `source` and `destination` objects,
which as the names suggest, tell you where the funds are coming from and where
they're going.
Sources and destinations can have the following types:
* `wire` for wire payments and payouts
* `blockchain` for transfers to/from blockchain addresses
* `wallet` for transfers to/from a Circle `wallet`
```json JSON theme={null}
// "blockchain"
// A "chain" and "address" together represent the blockchain location.
{
"type": "blockchain",
"address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F",
"chain": "ETH"
}
// "wallet"
// The "id" is the id of the Circle wallet.
{
"type": "wallet",
"id": "12345"
}
```
### Money object
Monetary amounts across Circle APIs are represented as `money` objects, which
consist of an `amount` and a `currency`. Two currencies are supported, `USD` and
`EUR`, with the amount represented as a string containing the whole units and
two decimals. In the example below, the amount is represented as 3.14 and the
currency as `USD`.
```json JSON theme={null}
{
"amount": "3.14",
"currency": "USD"
}
```
### Blockchain addresses
```json JSON theme={null}
{
"address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F",
"currency": "USD",
"chain": "ETH"
}
```
# Circle APIs: API and Entity Errors
Source: https://developers.circle.com/circle-mint/circle-apis-api-errors
Circle APIs use two kinds of error codes:
* **API response errors** return right away with the HTTP response.
* **Entity errors** attach to resources such as payments. They usually show up
seconds or minutes later.
## Error responses
When an API request is made and the error is known immediately, Circle APIs will
return the appropriate HTTP status code along with a JSON error response.
Because HTTP status codes do not always provide sufficient information about the
cause of an error, Circle's API errors provide more detailed JSON responses to
help you determine what's gone wrong.
In the example below, the request is missing a required field. The HTTP status
code in this case would be `HTTP/1.1 400 Bad Request`
```json JSON theme={null}
{
"code": 2,
"message": "Invalid entity. metadata.email may not be empty (was null)",
"errors": [
{
"error": "invalid_value",
"location": "metadata.email",
"message": "metadata.email may not be empty (was null)"
}
]
}
```
### Error types
You can automate the handling of certain error types. These include the
following error names, which include example messages and constraints, where
applicable:
| error | message | constraints |
| :---------------------- | :---------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- |
| `value_must_be_true` | field1 must be true (was false) | NA |
| `value_must_be_false` | field1 must be false (was true) | NA |
| `required` | field1 may not be null (was null) | NA |
| `not_required` | field1 must be null (was foo) | NA |
| `min_value` | field1 must be greater than or equal to 2 (was 0) | `"constraints": { "min": 2 }` |
| `min_value` | field1 must be greater than 2.0 (was 2.0) | `"constraints": { "min": "2.0", "inclusive": false }` |
| `max_value` | field1 must be less than or equal to 99 (was 1024) | `"constraints": { "max": 99 }` |
| `max_value` | field1 be less than or equal to 99.99 (was 100) | `"constraints": { "max": "99.99", "inclusive": true }` |
| `length_outside_bounds` | field1 must be between 2 and 5 (was qwerty) | `"constraints": { "min": 2, "max": 5 }` |
| `pattern_mismatch` | field1 must match "\[a-z]+" (was 123456) | `"constraints": { "pattern": "[a-z]+" }` |
| `date_not_in_past` | field1 must be in the past (was 2020-08-31T18:18:54.211Z) | NA |
| `date_not_in_future` | field1 must be in the future (was 2020-08-31T18:18:17.621Z) | NA |
| `number_format` | field1 numeric value out of bounds (\<3 digits>.\<2 digits> expected) (was 123.456) | `"constraints": { "max-integral-digits": 3, "max-fractional-digits": 2 }` |
### API error format
Whenever an API request results in an error, the JSON response contains both an
error code and a human-readable message in the body.
```json JSON theme={null}
{
"code": 2,
"message": "Invalid entity.\n[...\n]"
}
```
### Extended error format
In some cases, you'll receive extended information about why a request has
failed. For example, if you fail to supply a value for a required field, you'll
receive the following error response:
```json JSON theme={null}
{
"code": 2,
"message": "Invalid entity.\nfield1 may not be null (was null)",
"errors": [
{
"error": "required",
"message": "field1 may not be null (was null)",
"location": "field1",
"invalidValue": "null",
"constraints": {}
}
]
}
```
This extended error response contains one or more associated error descriptions.
Error description attributes can be read as follows:
| Key | Description | optional |
| :------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------- |
| `error` | type of an error | false |
| `message` | human-friendly message | false |
| `location` | period-separated path to the property that causes this error. An example could be `field1` or `address.billingCountry` | false |
| `invalidValue` | actual value of the property specified in `location` key, as received by the server | true |
| `constraints` | special object that contains additional details about the error and could be used for programmatic handling on the client side | true |
### List of API error codes
The table below shows the list of JSON error codes that may be returned in an
API error response.
| Code | Message | Description |
| -------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `-1` | Unknown Error | An unknown error occurred processing the API request |
| `1` | Malformed authorization. Is the authorization type missing? | API Key is missing or malformed |
| `2` | Invalid Entity | Error with the JSON object passed in the request body |
| `3` | Forbidden | API Key used with request does not have one of the roles authorized to call the API endpoint |
| `1077` | Payment amount is invalid | Payment amount must be greater than zero |
| `1078` | Payment currency not supported | An invalid currency value was used when making a payment |
| `1083` | Idempotency key already bound to other request | The idempotency key used when making a request was used with another payment. Retry with a different value |
| `1084` | This item cannot be canceled | A cancel or refund request cannot be canceled |
| `1085` | This item cannot be refunded | A cancel or refund request cannot be refunded |
| `1086` | This payment was already canceled | This payment was already canceled |
| `1087` | Total amount to be refunded exceeds payment amount | Total amount to be refunded exceeds payment amount |
| `1088` | Invalid source account | An invalid source account was specified in a payout or transfer request |
| `1089` | The source account could not be found | Unable to find the source account specified in a payout or transfer request |
| `1093` | Source account has insufficient funds | The source account has insufficient funds for the payout or transfer amount |
| `1096` | Encryption key id could not be found | An encryption key id must be provided if request includes encrypted data |
| `1097` | Original payment is failed | Attempting to cancel or refund a failed payment |
| `1101` | Invalid country format | An invalid ISO 31660-2 country code was provided |
| `1106` | Invalid district format | Invalid district format, must be a 2 character value |
| `1107` | Payout limit exceeded | Payout can't be accepted as it would exceed the given limit |
| `1108` | Unsupported Country | Country not supported for customer |
| `1143` | Checkout session not found | The checkout session id passed in the request doesn't exist in the DB |
| `1144` | Checkout session is already in a completed state | The checkout session cannot be extended because it is already in a complete state |
| `2003` | The recipient address already exists | The blockchain address has already been associated with the account |
| `2004` | The address is not a verified withdrawal address | The blockchain address must first be verified before it can be used as a destination in a transfer request |
| `2005` | The address belongs to an unsupported blockchain | The blockchain type used as a transfer destination is not supported |
| `2006` | Wallet type is not supported | The wallet type specified when creating an end user wallet is not supported |
| `2007` | Unsupported transfer | A transfer from the provided source to the provided destination is not supported |
| `2024` | Address book identity is missing | Address book identity is required for this entity |
| `2025` | Address book ownership is missing | Address book ownership is required for this entity |
| `2026` | Address book VASP ID is missing | Address book VASP ID is required for this entity |
| `2027` | Address book ownership type is invalid | Address book ownership type is not supported for this entity |
| `2028` | Address book custody type is invalid | Address book custody type is not supported for this entity |
| `2029` | Address book custody is missing | Address book custody is required for this entity |
| `2030` | Address book VASP ID is invalid | Address book VASP ID is not valid for this entity |
| `2031` | Address book identity type is invalid | Address book identity type must be either individual or business |
| `2032` | Address book identity first name is missing | First name is required for individual identity |
| `2033` | Address book identity last name is missing | Last name is required for individual identity |
| `2034` | Address book identity business name is missing | Business name is required for business identity |
| `2035` | Address book VASP ID is not allowed | VASP ID must not be provided for self-hosted custody |
| `2036` | Identity is not allowed in address book patch requests | The identity field cannot be updated via the patch endpoint |
| `2037` | Ownership is not allowed in address book patch requests | The ownership field cannot be updated via the patch endpoint |
| `5000` | Invalid travel rule identity type | The provided identity type must be either "individual" or "business" |
| `5001` | Payout not found | Payout doesn't exist based on the ID provided. Verify the payout ID. |
| `5002` | Invalid payout amount | Payout amount must be more than 0 |
| `5003` | Inactive destination address | Cannot send payout to an inactive destination address. If you have just added the address, you may have to wait for 24 hours before use |
| `5004` | Destination address not found | The destination address for this payout could not be found |
| `5005` | Source wallet not found | Source wallet for this payout could not be found |
| `5006` | Insufficient funds | The source wallet has insufficient funds for this payout |
| `5007` | Unsupported currency | Currency not supported for this operation |
| `5011` | Invalid destination address | Can't send payout to an invalid destination address |
| `5012` | Invalid destination location types | Can't search for both crypto and fiat payouts |
| `5013` | Invalid source wallet id | Source wallet id must be a number for payouts search |
| `5014` | The address is not valid for the blockchain | Provided blockchain address is not valid for the corresponding blockchain |
| `5015` | Invalid destination chain | Provided blockchain address has an invalid chain in respect to the currency used |
| `5020` | Invalid purpose of transfer | The `purposeOfTransfer` field is missing or invalid for crypto payouts |
| `500000` | Unsupported currency for reporting daily user balance | User provided a currency other than USDC or EURC |
| `500001` | Custody balance `asOfDate` is too far from today's date | `asOfDate` provided was before or after today's date |
| `500002` | Custody balance cannot be less than zero | Negative balances were provided |
| `500003` | Local custody balance can't be greater than total custody balance | Total balance must be greater than or equal to local balance |
| `500004` | Idempotency key can't be reused | The same idempotency key was used for different requests |
| `500005` | Custody balance report already exists | A custody balance report was already created for the provided date and currency. Contact customer care for edits. |
## Entity error responses
With entities such as payments, cards, bank wires, and transfers, an error is
generally not known immediately, so no error code can be returned at the time of
the request.
For instance, when making a payment, the transaction is processed asynchronously
and the create payment request will have a status of `pending`. After
processing, the status will eventually be set to `approved` or `failed`.
API entities such as payments and cards are processed asynchronously after the
initial create request is made. If a problem occurs while processing the request
(the status is shown as `failed`), the `errorCode` property is set on the entity
and can be retrieved either by polling the `GET` endpoint or via a notification.
The response explains why the payment was unsuccessful.
Here are some Entity error codes associated with payments, cards, bank wires,
and transfers.
### Payment error codes
| Code | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `payment_failed` | Payment failed due to an unknown reason |
| `payment_fraud_detected` | Suspected fraud detected by issuing bank. Instruct your user to contact their bank directly to resolve |
| `payment_denied` | Payment denied by Circle Risk Service, see payment `riskReasonCode` for more details |
| `payment_not_supported_by_issuer` | Issuer bank was unable to process the transaction |
| `payment_not_funded` | There were insufficient funds to cover the payment amount |
| `payment_unprocessable` | The provided `encryptedData` could not be processed |
| `payment_stopped_by_issuer` | A stop has been placed by the issuer or customer |
| `payment_canceled` | Payment was canceled |
| `payment_returned` | Payment was returned |
| `payment_failed_balance_check` | Payment failed the Plaid balance check due to insufficient funds |
| `card_failed` | Card payment failed |
| `card_invalid` | The card is invalid |
| `card_address_mismatch` | Card billing address does not match |
| `card_zip_mismatch` | Card billing ZIP code does not match |
| `card_cvv_invalid` | The card CVV is invalid |
| `card_expired` | The card has expired |
| `card_limit_violated` | Card limit was exceeded |
| `card_not_honored` | Card was not honored by the issuer |
| `card_cvv_required` | Card CVV is required but was not provided |
| `card_restricted` | Card use is restricted |
| `card_account_ineligible` | Card account is not eligible for this transaction |
| `card_network_unsupported` | The card network is not supported |
| `channel_invalid` | The payment channel is invalid |
| `unauthorized_transaction` | Transaction was unauthorized |
| `bank_account_ineligible` | The bank account is not eligible for this transaction |
| `bank_transaction_error` | There was an error processing the transaction on the bank |
| `invalid_account_number` | The bank account number is invalid |
| `invalid_wire_rtn` | The wire RTN is invalid |
| `invalid_ach_rtn` | The ACH RTN is invalid |
| `ref_id_invalid` | The reference ID is invalid |
| `account_name_mismatch` | The name on the account does not match |
| `account_number_mismatch` | The account number does not match |
| `account_ineligible` | The account is not eligible for this transaction |
| `wallet_address_mismatch` | The wallet address does not match |
| `customer_name_mismatch` | The customer name does not match |
| `institution_name_mismatch` | The institution name does not match |
| `vendor_inactive` | The vendor is inactive |
### Payout error codes
| Code | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `insufficient_funds` | Insufficient funds |
| `transaction_denied` | Transaction was denied by Circle Risk Service, see payout `riskEvaluation` for more details |
| `transaction_failed` | Transaction failed due to an unknown reason |
| `transaction_returned` | Transaction was returned |
| `bank_transaction_error` | There was an error processing the transaction on the bank |
| `fiat_account_limit_exceeded` | The fiat account limit was exceeded |
| `invalid_bank_account_number` | The bank account number is invalid |
| `invalid_ach_rtn` | The ACH RTN is invalid |
| `invalid_wire_rtn` | The wire RTN is invalid |
| `vendor_inactive` | The vendor is inactive |
### Transfer error codes
| Code | Description |
| -------------------- | ---------------------------------------------------------------------------------------------- |
| `transfer_failed` | The transfer failed due to unknown reasons |
| `transfer_denied` | The transfer was denied by Circle Risk Service, see transfer `riskEvaluation` for more details |
| `blockchain_error` | There was an error processing the transfer onchain |
| `insufficient_funds` | There was not enough funding to cover the transfer amount |
# Notifications Quickstart
Source: https://developers.circle.com/circle-mint/circle-apis-notifications-quickstart
Configure a notification subscriber endpoint and be notified when a payment is received.
*Circle notifications will inform you every time the status of a resource
changes, such as changes in payment status. Notifications can be accessed by
setting up a notification subscriber endpoint on your end. This quickstart guide
shows how to set up notifications for the Circle APIs. Follow this to configure
a subscriber endpoint that sends a notification each time the resource status
changes.*
Note: This guide addresses the payment resource, but is applicable to any of the
resources mentioned in
[Circle API Notifications](/circle-mint/notifications-data-models).
## 1. Expose a Subscriber Endpoint
To receive notifications on changes in resource status, you must expose a
publicly accessible subscriber endpoint on your side. The endpoint must handle
both `HEAD` and `POST` requests over HTTPS.
To expose an endpoint for testing, you can use
[webhook.site](https://webhook.site/) to inspect, test and automate incoming
HTTPS requests or e-mails directly in the web browser.
**💡 Tip:**
When you visit [webhook.site](https://webhook.site/) for the first time, you
should see a status message that looks similar to the following:
`Your unique URL (Please copy it from here, not from the address bar!) https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7`
Navigate to [webhook.site](https://webhook.site/) and record the value of the
URL shown as `Your unique URL`:
In the example above, the unique URL
is:`https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7`. Use the
public-facing URL you receive as you progress throughout this guide.
## 2. Subscribe to Status Notifications
Now that you have a publicly accessible endpoint, you need to register your
endpoint as a subscriber to webhook notifications by doing the following:
1. Navigate to **API > Subscriptions** in your Circle Mint account and click
**Add Subscription**.
2. Enter your endpoint URL from above. It will be similar to the earlier
example: `https://webhook.site/83fa21a0-f00a-4673-bb50-bcf62c78b1f7`.
3. Click **Add Subscription**:
4. You should receive two responses on your local server shell that confirm the
subscription with a body similar to the following:
```json JSON theme={null}
{
"Type": "SubscriptionConfirmation",
"MessageId": "ddbdcdcf-d36a-45b5-927c-da25b9b009ae",
"Token": "2336412f37fb687f5d51e6e2425f004aed7b7526d5fae41bc257a0d80532a6820258bf77eb25b90453b863450713a2a5a4250696d725a306ef39962b5b543752c9003e0841c0e61253fd6c517a94edebe44f36c5fe4ba131c8ea5f6f42a43f97f6e1865505e2f29f79a62f89e18f97e03a0dd5d982a7578c8d6e21154163f2d6aae523cff25557f9bc21b2503d413006",
"TopicArn": "arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic",
"Message": "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
"SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:908968368384:sandbox_platform-notifications-topic&Token=2336412f37fb687f5d51e6e2425f004aed7b7526d5fae41bc257a0d80532a6820258bf77eb25b90453b863450713a2a5a4250696d725a306ef39962b5b543752c9003e0841c0e61253fd6c517a94edebe44f36c5fe4ba131c8ea5f6f42a43f97f6e1865505e2f29f79a62f89e18f97e03a0dd5d982a7578c8d6e21154163f2d6aae523cff25557f9bc21b2503d413006",
"Timestamp": "2020-04-11T20:50:16.324Z",
"SignatureVersion": "1",
"Signature": "kBr9z/ysQrr0ldowHY4lThkOA+dwyjcsyx7NwkbTkgEKG4N61BSSEA+43aYQEB/Ml09hclybvyjyRKWYOjaxQgbUXWmyWrCQ7vY93WYhuGvOqZxAMPiDiILxLs6/KtOxneKVvzfpK4abLrYyTTA+z/dQ52h9L8eoiSKSW81e4clfYBTJkGmuAPKFC08FvEAVT89VikPp68mBf4CctPv3Em0b4J1VvDhAB21B2LekgUmwUE0aE7fUbsF3XsKGQd/fDshLOJasQEuXSqdB5X7LITBA8r24FY+wCjwm8oR3VI9IMy21fUC6wMgoFIVZHW1KxzpEkMCSe7R1ySdNIru8SQ==",
"SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem"
}
```
**Note:** The response body above is in a plain text format, so be sure you're
not expecting a JSON content type.
5. To complete the subscription process, visit the `SubscribeURL` link in each
response. Messages won't be sent to the endpoint until you confirm the
subscription by accessing the URL.
6. After you visit the `SubscribeURL` links, the subscription status updates to
`COMPLETE`, indicating it's ready for use.
**Note:** In the Circle sandbox environment, if your subscription is stuck in
the `PENDING` (unconfirmed) state, reach out to
[customer-support@circle.com](mailto:customer-support@circle.com) to remove
it.
In the production environment, if your subscription is stuck in the `PENDING`
state, it's automatically removed after 72 hours.
You now have a sample local environment ready to receive notifications.
# Circle SDKs
Source: https://developers.circle.com/circle-mint/circle-sdks
Use the TypeScript SDK to quickly get your codebase up and running with Circle APIs.
Circle's SDKs help you integrate Circle APIs. The TypeScript SDK is open source.
If you find a bug or need a feature, file an issue or contribute to the
[`node-sdk`](https://github.com/circlefin/circle-nodejs-sdk).
**Note:** This page contains code snippets and examples that can be run as is or
incorporated into your apps.
## Installing the SDK
Run one of the following commands from your project directory to install the
SDK.
```shell Node theme={null}
npm install @circle-fin/circle-sdk --save
# or
yarn add @circle-fin/circle-sdk
```
## Configuration
```typescript theme={null}
import { Circle, CircleEnvironments } from "@circle-fin/circle-sdk";
const circle = new Circle(
"",
CircleEnvironments.sandbox, // API base url
);
```
## List balances
```typescript theme={null}
async function listBalances() {
const balancesRes = await circle.balances.listBalances();
}
```
## Create a crypto payment
```typescript theme={null}
async function createCryptoPayment() {
const createCryptoPaymentRes =
await circle.paymentIntents.createPaymentIntent({
idempotencyKey: "5c6e9b91-6563-47ec-8c6d-0ce1103c50b3",
amount: {
amount: "3.14",
currency: "USD",
},
settlementCurrency: "USD",
paymentMethods: [
{
chain: "ETH",
type: "blockchain",
},
],
});
}
```
## Get a crypto payment
```typescript theme={null}
async function getCryptoPayment(id: string) {
const cryptoPayment = await circle.paymentIntents.getPaymentIntent(id);
}
```
# How Minting and Redemption Works
Source: https://developers.circle.com/circle-mint/concepts/how-minting-works
Understand how Circle Mint converts fiat to USDC (minting) and USDC back to fiat (redemption), including settlement timing, account structure, and compliance requirements.
Circle Mint's core operations are minting and redemption. Minting converts fiat
currency into stablecoins (USDC or EURC), and redemption converts stablecoins
back to fiat. Every token mints and redeems at a 1:1 ratio with the underlying
fiat currency. This page explains the mental model behind these operations,
including settlement timing, account structure, onchain transfers, fees, and
compliance.
## Minting
Minting (also known as an onramp) is the process of depositing fiat currency and
receiving an equivalent amount of stablecoins. When you send a fiat transfer to
Circle, Circle credits your Mint account balance with the corresponding
stablecoin amount at a 1:1 ratio.
The minting flow works as follows:
1. You initiate a fiat transfer from your linked bank account to Circle.
2. Circle receives the fiat deposit and credits your Mint account balance.
3. The stablecoins become available for onchain transfers or other operations.
Circle supports multiple payment rails for fiat deposits, including standard
wires (FedWire and SWIFT), real-time interbank rails (RTP, SPEI, SEPA, and
CHATS) in supported regions, and book transfers when you bank with one of
Circle's settlement partners. Rail availability depends on your region and the
currency you are depositing.
**Settlement timing:** Domestic wire deposits received before the daily cutoff
settle on the same business day. Real-time interbank rails settle in seconds,
subject to network operating hours and transaction limits. International wires
take 1-3 business days depending on intermediary banks. In the sandbox
environment, mock wire deposits process in batches and may take up to 15
minutes.
For step-by-step instructions, see
[Deposit Fiat](/circle-mint/howtos/deposit-fiat).
## Redemption
Redemption (also known as an offramp) is the reverse of minting. You convert
stablecoins back to fiat currency by creating a payout to a linked bank account.
The redemption flow works as follows:
1. You create a payout request specifying the amount and destination bank
account.
2. Circle debits the stablecoin amount from your Mint account balance.
3. Circle sends a fiat transfer to your bank account using the appropriate rail
for your region and bank.
**Settlement timing:** Payouts typically settle on the next business day. In
some cases, your bank may reject the incoming wire, resulting in a returned
withdrawal. If a payout is returned, the funds are credited back to your Mint
account balance.
For step-by-step instructions, see
[Withdraw Fiat](/circle-mint/howtos/withdraw-fiat).
## Account structure
Your Circle Mint account has several key components that work together to
support minting, redemption, and onchain transfers.
### Primary wallet
Every Circle Mint account has a primary wallet identified by a `masterWalletId`.
You retrieve this identifier from the `/v1/configuration` endpoint. The primary
wallet serves as the source for outbound transfers and the destination for
inbound deposits.
### Balances
Your account maintains two types of balances:
* **Available balance:** Settled funds you can transfer or redeem immediately.
* **Unsettled balance:** Funds that are in transit and not yet available. Wire
deposits appear as unsettled until they clear.
### Linked bank accounts
You register external bank accounts to send and receive fiat. Each linked bank
account receives a unique Virtual Account Number (VAN). When you wire funds to
Circle using the VAN, Circle attributes the deposit to your account without
requiring a tracking reference in the payment instruction.
### Deposit addresses
Circle generates one deposit address per blockchain for your account. These
addresses receive inbound stablecoin transfers from external wallets. You
retrieve your deposit addresses through the API for each
[supported blockchain](/circle-mint/references/supported-chains-and-currencies).
### Recipient addresses
Recipient addresses are external blockchain addresses that you register and
allowlist for outbound transfers. You must create a recipient address before you
can send stablecoins to it. This allowlisting step provides an additional layer
of security for outbound transfers.
## Onchain transfers
Circle Mint supports both receiving and sending stablecoins onchain.
* **Receiving:** External wallets send USDC or EURC to your deposit address on
any supported blockchain. Circle detects the transfer and credits your account
after the required number of
[blockchain confirmations](/circle-mint/references/blockchain-confirmations).
* **Sending:** You create a transfer to a registered recipient address. Circle
debits your balance and broadcasts the transaction onchain.
### Transfer status lifecycle
Onchain transfers progress through the following statuses:
| Status | Description |
| ---------- | ------------------------------------------------------------------- |
| `pending` | The transfer request is created but not yet broadcast onchain. |
| `running` | The transaction is broadcast and awaiting blockchain confirmations. |
| `complete` | The required confirmations are reached and the transfer is final. |
For details on confirmation requirements per blockchain, see
[Blockchain Confirmations](/circle-mint/references/blockchain-confirmations).
For step-by-step transfer instructions, see
[Transfer USDC Onchain](/circle-mint/howtos/transfer-on-chain).
## Network fees
Circle covers gas fees for outbound stablecoin transfers in most cases. You do
not need to hold the native token of each blockchain to send USDC or EURC from
your Mint account.
## Travel Rule compliance
Transfers of \$3,000 or more in value on supported blockchains are subject to the
FinCEN Travel Rule, which requires identity data about the originator of the
transaction.
How Circle handles the identity requirement depends on the type of transfer:
* **Business account transfers:** Circle uses your company's identity stored on
file. You do not need to include identity data in each request.
* **Third-party payouts:** If you send funds on behalf of someone else, you must
provide the originator's identity (name and address) in the payout request.
Omitting required identity data causes the transfer to fail.
For the full list of supported blockchains and implementation details, see
[Travel Rule compliance](/circle-mint/howtos/transfer-on-chain#travel-rule-compliance).
## Approval workflows
Customers in France and Singapore are subject to additional recipient address
verification requirements. Before an outbound transfer can proceed, the
recipient address must be verified through the
[Mint Console](https://app.circle.com/signin). This approval step ensures
compliance with local regulatory requirements in those jurisdictions.
# Credit API
Source: https://developers.circle.com/circle-mint/credit-api/overview
Programmatically manage Settlement Advance and Line of Credit products through your Circle Mint account.
The Credit API provides programmatic access to Circle Mint's credit products.
You can initiate draws, track transfer status, manage fees, and process
repayments through a dedicated set of endpoints under `/v1/credit/`.
The Credit API is available to institutional Circle Mint customers with an
approved credit facility. A separate offline credit contract through Circle is
required before you can access these endpoints.
[Contact Circle](https://www.circle.com/mint-contact) to learn more about credit
products.
## Credit products
Bridges settlement timing gaps through a reserve-then-request flow. You
reserve funds, upload wire proof, and wait for Treasury approval before
disbursement.
Provides single-request draws with auto-approval and asynchronous
disbursement. No wire proof required. Supports crypto repayment directly
from your Circle Mint wallet.
## Key concepts
* **Credit line**: Your facility object, including credit limit, used and
available amounts, fee rates, minimum balance requirement, and validation
errors. Each customer has one credit line.
* **Credit transfers**: Individual draw requests against your credit line.
Settlement Advance transfers progress through `funds_reserved`, `requested`,
`disbursed`, and `paid`. Line of Credit transfers progress through
`requested`, `disbursed`, and `paid`. Both can reach `past_due` if unpaid
after the due date. Transfers on daily cadence have a 7-day repayment due date
from disbursement, and transfers on hourly cadence have a 24-hour repayment
due date from disbursement.
* **Fees**: Each draw can include two types of fees: a draw fee (Settlement
Advance only) charged at disbursement and a recurring fee accrued on a cadence
determined by your credit line's `feeCadence` setting: daily (every 24 hours)
or hourly (every hour). The `recurringFee` rate in `feeRates` applies at
whichever cadence your credit line uses. Hourly cadence is available only on
Line of Credit. Fees are auto-deducted from your Circle Mint wallet balance.
* **Minimum balance**: A required USDC balance (`minBalance`) that you must
maintain in your Circle Mint wallet while you have an active credit line.
* **Validation errors**: Conditions that block new transfer creation, such as
`INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`. Returned in
the credit line response so you can resolve them before initiating a draw.
* **Blockchain destination**: An optional feature that sends disbursed funds
directly to a verified blockchain address instead of your Circle Mint wallet.
Include the `destination` field when creating a transfer, referencing an
address from your Circle Mint recipient address book. If omitted, funds are
deposited into your Circle Mint wallet by default. To manage verified
recipient addresses, use the
[recipient address endpoints](/api-reference/circle-mint/account/create-business-recipient-address).
## Get started
Reserve funds, upload wire proof, and track your advance through
disbursement
Request a draw, monitor its status, and repay using crypto
# Quickstart: Line of Credit
Source: https://developers.circle.com/circle-mint/credit-api/quickstart-line-of-credit
Request a draw, track disbursement, and repay outstanding balances using the Credit API's Line of Credit endpoints.
This quickstart walks you through a complete Line of Credit draw and repayment
lifecycle using the Credit API. You check your available credit, request a draw,
track the transfer through disbursement, and repay using USDC from your Circle
Mint wallet.
## Prerequisites
Before you begin, ensure you have:
* An approved Line of Credit facility with Circle. Contact your Circle
representative if you haven't completed the offline credit agreement.
* A Circle Mint API key with the Credit API entitlement enabled.
* Familiarity with
[Circle Mint authentication](/circle-mint/quickstarts/getting-started) and
[idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests).
## Step 1: Check your credit line status
Before requesting a draw, verify that your credit line is active and has
sufficient available credit. Call the `GET /v1/credit` endpoint to retrieve your
credit line details.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
```json Response theme={null}
{
"data": {
"id": "b3d9d2d5-4c12-4946-a09d-953e82fae2b0",
"product": "lineOfCredit",
"feeCadence": "daily",
"status": "active",
"limit": { "amount": "1000000.00", "currency": "USD" },
"used": { "amount": "250000.00", "currency": "USD" },
"available": { "amount": "750000.00", "currency": "USD" },
"outstandingTransfers": 2,
"feeRates": { "recurringFee": "0.0003" },
"unpaidFees": { "amount": "150.00", "currency": "USD" },
"minBalance": { "amount": "100000.00", "currency": "USD" },
"validationErrors": [],
"createDate": "2024-01-15T10:30:00.000Z",
"updateDate": "2024-03-20T14:22:00.000Z"
}
}
```
Confirm the following before proceeding:
* `product` is `lineOfCredit`.
* `status` is `active`.
* `available.amount` is greater than or equal to the amount you plan to draw.
* `validationErrors` is an empty array. If it contains errors such as
`INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`, resolve them
before you create a new transfer.
The `feeCadence` field indicates how frequently fees accrue on your credit line.
The `recurringFee` rate in `feeRates` applies at whichever cadence your credit
line uses.
* **Daily** (every 24 hours): Available on both products. Transfers have a 7-day
due date from disbursement.
* **Hourly** (every hour): Available only on Line of Credit. Transfers have a
24-hour due date from disbursement.
For an hourly cadence credit line, the relevant fields differ as follows:
```json Response (hourly cadence — other fields unchanged) theme={null}
{
"data": {
"feeCadence": "hourly",
"feeRates": { "recurringFee": "0.0000125" }
}
}
```
## Step 2: Request a draw
Request a draw against your credit line by calling `POST /v1/credit/transfers`
with an idempotency key and the desired amount. Unlike Settlement Advance
transfers, LoC draws are auto-approved and don't require wire proof or Treasury
review.
```bash theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/transfers' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"amount": { "amount": "50000.00", "currency": "USD" },
"destination": {
"type": "verified_blockchain",
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
}'
```
The `destination` field is optional. If included, disbursed funds are sent to
the specified verified blockchain address instead of your Circle Mint wallet.
The `addressId` must reference a verified address from your [Circle Mint
recipient address
book](/api-reference/circle-mint/account/create-business-recipient-address).
If you omit `destination`, funds are deposited into your Circle Mint wallet by
default.
```json Response theme={null}
{
"data": {
"id": "a1c2e3f4-5678-4d90-b123-456789abcdef",
"amount": { "amount": "50000.00", "currency": "USD" },
"status": "requested",
"blockchainDestination": {
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "pending"
},
"createDate": "2024-03-20T14:20:00.000Z",
"updateDate": "2024-03-20T14:20:00.000Z"
}
}
```
Save the transfer `id` from the response. You need it to track the transfer in
the next step.
The transfer starts in `requested` status and automatically transitions to
`disbursed` once the funds are deposited into your Circle Mint wallet, or sent
to the verified blockchain address if you specified a `destination`. No manual
approval step is required.
## Step 3: Track the transfer
Monitor your transfer as it progresses from `requested` to `disbursed`. You can
poll the transfer endpoint or subscribe to webhook notifications.
### Poll the transfer endpoint
Call `GET /v1/credit/transfers/{id}` to check the current transfer status.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/transfers/a1c2e3f4-5678-4d90-b123-456789abcdef' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
Once the transfer is disbursed, the response includes fee and repayment details:
```json Response theme={null}
{
"data": {
"id": "a1c2e3f4-5678-4d90-b123-456789abcdef",
"amount": { "amount": "50000.00", "currency": "USD" },
"status": "disbursed",
"outstanding": { "amount": "50150.00", "currency": "USD" },
"fees": {
"total": { "amount": "150.00", "currency": "USD" },
"unpaid": { "amount": "150.00", "currency": "USD" }
},
"dueDate": "2024-03-27T14:22:00.000Z",
"disbursedDate": "2024-03-20T14:22:00.000Z",
"blockchainDestination": {
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "complete",
"transferId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321"
},
"createDate": "2024-03-20T14:20:00.000Z",
"updateDate": "2024-03-21T00:00:00.000Z"
}
}
```
The `outstanding` amount includes the original draw plus any accrued fees. The
`dueDate` indicates when repayment is expected. For lines on daily cadence, the
due date is 7 days from disbursement. For lines on hourly cadence, the due date
is 24 hours from disbursement.
If you included a `destination` in your request, the `blockchainDestination`
field tracks the status of the onchain transfer.
| Status | Description | `transferId` populated |
| ----------- | ------------------------------------------- | ---------------------- |
| `pending` | Destination recorded, awaiting disbursement | No |
| `initiated` | Blockchain transfer submitted | Yes |
| `complete` | Funds arrived at destination address | Yes |
| `failed` | Blockchain transfer failed | Yes |
### Subscribe to webhook notifications
For real-time updates, subscribe to webhook notifications through the Circle
Mint webhook management UI:
* **`creditTransfers`**: Receive notifications whenever a transfer changes
status, such as the transition from `requested` to `disbursed`.
* **`creditFees`**: Receive notifications when fees are accrued against your
outstanding transfers.
* **`creditRepayments`**: Receive notifications when a repayment is received and
allocated against your outstanding balance.
The Line of Credit transfer follows this status lifecycle:
```mermaid theme={null}
stateDiagram-v2
[*] --> requested
requested --> disbursed
requested --> rejected
disbursed --> paid
disbursed --> past_due
past_due --> paid
```
A transfer can also reach `rejected` (if the request is declined) or `past_due`
(if repayment is overdue after the due date).
## Step 4: Repay outstanding balances
You can repay Line of Credit draws in two ways:
* **Crypto repayment**: Deduct USDC directly from your Circle Mint wallet
balance using the `POST /v1/credit/cryptoRepayment` endpoint.
* **Wire repayment**: Send a wire transfer to Circle using the account details
from `GET /v1/credit/repaymentAccounts/{fiatAccountId}`. You need a
`fiatAccountId` from your wire bank accounts (see
[list wire bank accounts](/api-reference/circle-mint/account/list-business-wire-accounts)).
Include the `trackingRef` value on your wire so Circle can match the payment
to your credit line. For details on retrieving repayment account details, see
the
[Settlement Advance quickstart](/circle-mint/credit-api/quickstart-settlement-advance#step-2-get-repayment-account-details).
The following example shows a crypto repayment. Call
`POST /v1/credit/cryptoRepayment` with the amount you want to apply toward your
outstanding balance.
```bash theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/cryptoRepayment' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"idempotencyKey": "d7a3e2c1-8f45-4b91-ae67-2c9d0f8b3e5a",
"amount": { "amount": "25000.00", "currency": "USD" }
}'
```
```json Response theme={null}
{
"data": {
"id": "e5f6a7b8-9012-3456-efab-345678901234",
"amount": { "amount": "25000.00", "currency": "USD" },
"status": "pending",
"createDate": "2024-03-25T10:00:00.000Z",
"updateDate": "2024-03-25T10:00:00.000Z"
}
}
```
The requested repayment amount is capped at your total outstanding balance. If
you specify an amount greater than what you owe, the API applies only the
outstanding balance. Crypto repayment is available exclusively for Line of
Credit products.
## Step 5: Verify repayment status
After initiating a repayment, verify that it completes successfully. You can
poll the repayments endpoint or subscribe to webhook notifications.
### Poll the repayments endpoint
Call `GET /v1/credit/repayments` to list all repayments and check their status.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/repayments' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
Once the repayment is processed, the response shows the completed payment with
the amount applied:
```json Response theme={null}
{
"data": [
{
"id": "d4e5f6a7-b890-1234-defa-234567890123",
"transferId": "a1c2e3f4-5678-4d90-b123-456789abcdef",
"amountApplied": { "amount": "25000.00", "currency": "USD" },
"paymentAmount": { "amount": "25000.00", "currency": "USD" },
"type": "crypto",
"status": "completed",
"settlementDate": "2024-04-08T12:00:00.000Z",
"createDate": "2024-04-08T12:00:00.000Z",
"updateDate": "2024-04-08T12:00:00.000Z"
}
]
}
```
The `transferId` field links the repayment to the original draw. The
`amountApplied` reflects the portion of the payment allocated to that transfer's
outstanding balance.
### Subscribe to webhook notifications
For real-time repayment tracking, subscribe to `creditRepayments` webhook
notifications through the Circle Mint webhook management UI. You receive
notifications when a repayment is received and allocated against your
outstanding transfers.
# Quickstart: Settlement Advance
Source: https://developers.circle.com/circle-mint/credit-api/quickstart-settlement-advance
Reserve funds, upload wire proof, and track your Settlement Advance through disbursement using the Credit API.
This quickstart walks you through a complete Settlement Advance draw lifecycle
using the Credit API. You check your available credit, reserve funds, upload
wire proof for Treasury review, and track the transfer through to disbursement.
## Prerequisites
Before you begin, ensure you have:
* An approved Settlement Advance credit facility with Circle. Contact your
Circle representative if you haven't completed the offline credit agreement.
* A Circle Mint API key with the Credit API entitlement enabled.
* Familiarity with
[Circle Mint authentication](/circle-mint/quickstarts/getting-started) and
[idempotent requests](/circle-mint/references/sandbox-and-testing#idempotent-requests).
## Step 1: Check your credit line status
Before initiating a draw, verify that your credit line is active and has
sufficient available credit. Call the `GET /v1/credit` endpoint to retrieve your
credit line details.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
```json Response theme={null}
{
"data": {
"id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32",
"product": "settlementAdvance",
"feeCadence": "daily",
"status": "active",
"limit": { "amount": "500000.00", "currency": "USD" },
"used": { "amount": "100000.00", "currency": "USD" },
"available": { "amount": "400000.00", "currency": "USD" },
"outstandingTransfers": 1,
"feeRates": { "drawFee": "0.0001", "recurringFee": "0.0003" },
"unpaidFees": { "amount": "0.00", "currency": "USD" },
"minBalance": { "amount": "50000.00", "currency": "USD" },
"validationErrors": [],
"createDate": "2024-01-15T10:30:00.000Z",
"updateDate": "2024-03-20T14:22:00.000Z"
}
}
```
Confirm the following before proceeding:
* `status` is `active`.
* `available.amount` is greater than or equal to the amount you plan to reserve.
* `validationErrors` is an empty array. If it contains errors such as
`INSUFFICIENT_BALANCE`, `PENDING_FEES`, or `OVERDUE_TRANSFERS`, resolve them
before you create a new transfer.
The `feeCadence` field indicates how frequently fees accrue on your credit line.
Settlement Advance credit lines always use `daily` cadence.
## Step 2: Get repayment account details
Before initiating a draw, retrieve the repayment account details for your wire
bank account. You need the beneficiary information and tracking reference to
send your wire.
First, identify the `fiatAccountId` of the wire bank account you want to use for
repayment. You can list your existing wire bank accounts with
`GET /v1/businessAccount/banks/wires` (see the
[list wire bank accounts](/api-reference/circle-mint/account/list-business-wire-accounts)
API reference).
Once you have the `fiatAccountId`, call
`GET /v1/credit/repaymentAccounts/{fiatAccountId}` to get the beneficiary
details and tracking reference for your wire.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/repaymentAccounts/b8627ae8-732b-4d25-b947-1df8f4007a29' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
```json Response theme={null}
{
"data": {
"id": "c9f2a1b3-6d84-4e0f-b512-9a8c7e3d4f01",
"description": "WELLS FARGO BANK, NA ****1111",
"status": "unverified",
"wireInstructions": {
"trackingRef": "CIR3XBZZ4N",
"beneficiary": {
"name": "CIRCLE INTERNET FINANCIAL INC",
"address1": "1 Main Street",
"address2": "Suite 1"
},
"beneficiaryBank": {
"swiftCode": "SVBKUS6S",
"routingNumber": "121140399",
"accountNumber": "3302726104",
"currency": "USD",
"name": "SILICON VALLEY BANK",
"address": "3003 TASMAN DRIVE",
"city": "SANTA CLARA",
"postalCode": "95054",
"country": "US"
}
}
}
}
```
Use the `trackingRef` value (at `data.wireInstructions.trackingRef`) as the
reference on your wire transfer so that Circle can match the incoming payment to
your credit line. Save the beneficiary bank details for use when initiating your
wire.
A `status` of `unverified` is expected for first-time use. The status becomes
`active` after a completed credit repayment is matched to this account.
**Sandbox only:** You can simulate an incoming wire payment without sending
real funds by calling the mock repayment endpoint:
```bash theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/mocks/repayments' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"fiatAccountId": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"amount": { "amount": "100000.00", "currency": "USD" }
}'
```
This creates a simulated wire arrival and auto-links the fiat account as a
repayment account if it is not already linked.
## Step 3: Reserve funds
The reserve step holds the requested amount for you while you initiate and
confirm your wire transfer.
Reserve funds against your credit line by calling
`POST /v1/credit/transfers/reserveFunds`. This creates a transfer in
`funds_reserved` status and holds the requested amount against your available
credit.
```bash theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/credit/transfers/reserveFunds' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"amount": { "amount": "100000.00", "currency": "USD" },
"destination": {
"type": "verified_blockchain",
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
}'
```
The `destination` field is optional. If included, disbursed funds are sent to
the specified verified blockchain address instead of your Circle Mint wallet.
The `addressId` must reference a verified address from your [Circle Mint
recipient address
book](/api-reference/circle-mint/account/create-business-recipient-address).
If you omit `destination`, funds deposit into your Circle Mint wallet by
default.
```json Response theme={null}
{
"data": {
"id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32",
"amount": { "amount": "100000.00", "currency": "USD" },
"status": "funds_reserved",
"expiresAt": "2024-03-20T15:00:00.000Z",
"blockchainDestination": {
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "pending"
},
"createDate": "2024-03-20T14:30:00.000Z",
"updateDate": "2024-03-20T14:30:00.000Z"
}
}
```
Save the transfer `id` from the response. You need it for the remaining steps.
Reserved funds expire after 30 minutes. If you don't upload wire proof before
the `expiresAt` timestamp, the reservation automatically transitions to
`expired` status and the funds return to your available credit.
Only one transfer can be in `funds_reserved` status per credit line at a time.
You must complete or cancel the current reservation before creating a new one.
## Step 4: Upload wire proof
Using the beneficiary details and tracking reference from Step 2 (found at
`data.wireInstructions.trackingRef` in the response), initiate your wire
transfer through your bank. Then upload proof of the wire to request
disbursement. Call `PUT /v1/credit/transfers/{id}/requestReservedFunds` with a
`multipart/form-data` request containing your wire proof document.
Accepted file types are PDF (`application/pdf`), JPEG (`image/jpeg`), and PNG
(`image/png`).
```bash theme={null}
curl --location --request PUT 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32/requestReservedFunds' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--form 'fileName="wire-confirmation.pdf"' \
--form 'fileContent=@"/path/to/wire-confirmation.pdf"'
```
```json Response theme={null}
{
"data": {
"id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32",
"amount": { "amount": "100000.00", "currency": "USD" },
"status": "requested",
"createDate": "2024-03-20T14:30:00.000Z",
"updateDate": "2024-03-20T14:45:00.000Z"
}
}
```
The transfer status transitions from `funds_reserved` to `requested`. At this
point, Circle's Treasury team reviews the wire proof. No further action is
required from you until the review completes.
## Step 5: Track the transfer status
Monitor your transfer as it progresses through the review and disbursement
process. You can poll the transfer endpoint or subscribe to webhook
notifications.
### Poll the transfer endpoint
Call `GET /v1/credit/transfers/{id}` to check the current transfer status.
```bash theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
Once Treasury approves the transfer and disburses funds, the response reflects
the `disbursed` status along with fee and repayment details:
```json Response theme={null}
{
"data": {
"id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32",
"amount": { "amount": "100000.00", "currency": "USD" },
"status": "disbursed",
"outstanding": { "amount": "102000.00", "currency": "USD" },
"fees": {
"total": { "amount": "2000.00", "currency": "USD" },
"unpaid": { "amount": "2000.00", "currency": "USD" }
},
"dueDate": "2024-03-28T10:00:00.000Z",
"disbursedDate": "2024-03-21T10:00:00.000Z",
"blockchainDestination": {
"addressId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "complete",
"transferId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321"
},
"createDate": "2024-03-20T14:30:00.000Z",
"updateDate": "2024-03-21T10:00:00.000Z"
}
}
```
The `outstanding` amount includes the original draw plus any accrued fees. The
`dueDate` indicates when repayment is expected. Settlement Advance transfers use
daily cadence with a 7-day repayment due date from disbursement.
If you included a `destination` in your request, the `blockchainDestination`
field tracks the status of the onchain transfer.
| Status | Description | `transferId` populated |
| ----------- | ------------------------------------------- | ---------------------- |
| `pending` | Destination recorded, awaiting disbursement | No |
| `initiated` | Blockchain transfer submitted | Yes |
| `complete` | Funds arrived at destination address | Yes |
| `failed` | Blockchain transfer failed | Yes |
### Subscribe to webhook notifications
For real-time updates, subscribe to webhook notifications through the Circle
Mint webhook management UI:
* **`creditTransfers`**: Receive notifications whenever a transfer changes
status, such as the transition from `requested` to `disbursed`.
* **`creditFees`**: Receive notifications when fees are accrued against your
outstanding transfers.
* **`creditRepayments`**: Receive notifications when a repayment is received and
allocated against your outstanding balance.
The Settlement Advance transfer follows this status lifecycle:
```mermaid theme={null}
stateDiagram-v2
[*] --> funds_reserved
funds_reserved --> requested
funds_reserved --> expired
funds_reserved --> canceled
requested --> disbursed
requested --> rejected
disbursed --> paid
disbursed --> past_due
past_due --> paid
```
A transfer can also reach `expired` (if the reservation times out), `rejected`
(if Treasury declines the request), or `past_due` (if repayment is overdue).
In sandbox, Settlement Advance requests are auto-approved. To simulate a
rejection, reserve funds with an amount of `119.53`.
## Cancel a reservation (optional)
If you need to cancel a reservation before uploading wire proof, call
`PUT /v1/credit/transfers/{id}/cancelReserve`. You can only cancel a transfer
that is in `funds_reserved` status.
```bash theme={null}
curl --location --request PUT 'https://api-sandbox.circle.com/v1/credit/transfers/fc988ed5-c129-4f70-a064-e5beb7eb8e32/cancelReserve' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
```json Response theme={null}
{
"data": {
"id": "fc988ed5-c129-4f70-a064-e5beb7eb8e32",
"amount": { "amount": "100000.00", "currency": "USD" },
"status": "canceled",
"createDate": "2024-03-20T14:30:00.000Z",
"updateDate": "2024-03-20T14:50:00.000Z"
}
}
```
After cancellation, the reserved amount returns to your available credit and you
can create a new reservation.
# Crypto Payouts Payment Reason Codes
Source: https://developers.circle.com/circle-mint/crypto-payouts-payment-reason-codes
Payment reason codes for the Crypto Payouts API purposeOfTransfer field.
Use a `purposeOfTransfer` value when you
[create a crypto payout](/api-reference/circle-mint/payouts/create-payout), if
your entity configuration requires it. Each value is a payment reason code.
The table lists codes accepted for Crypto Payouts. Values align with
[CPN payment reason codes](/cpn/references/errors/payment-reason-codes). This
list adds `PMT000` for cases that do not match another code. Do not use
`PMT006`; it is not valid for crypto payouts.
| Reason code | Description |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| `PMT000` | Others |
| `PMT001` | Invoice payment |
| `PMT002` | Payment for services |
| `PMT003` | Payment for software |
| `PMT004` | Payment for imported goods |
| `PMT005` | Travel services |
| `PMT007` | Repayment of loans |
| `PMT008` | Payroll |
| `PMT009` | Payment of property rental |
| `PMT010` | Information service charges |
| `PMT011` | Advertising and public relations related expenses |
| `PMT012` | Royalty fees, trademark fees, patent fees, and copyright fees |
| `PMT013` | Fees for brokers, front end fee, commitment fee, guarantee fee, and custodian fee |
| `PMT014` | Fees for advisors, technical assistance, and academic knowledge including remuneration for specialists |
| `PMT015` | Representative office expenses |
| `PMT016` | Tax payment |
| `PMT017` | Transportation fees for goods |
| `PMT018` | Construction costs/expenses |
| `PMT019` | Insurance premium |
| `PMT020` | General goods trades (offline) |
| `PMT021` | Insurance claims payment |
| `PMT022` | Remittance payments to friends or family |
| `PMT023` | Education-related student expenses |
| `PMT024` | Medical treatment |
| `PMT025` | Donations |
| `PMT026` | Mutual fund investment |
| `PMT027` | Currency exchange |
| `PMT028` | Advance payments for goods |
| `PMT029` | Merchant settlement |
| `PMT030` | Repatriation fund settlement |
# Quickstart: Exchange Local Currency for USDC
Source: https://developers.circle.com/circle-mint/exchange-local-currency-usdc
The Cross-Currency API lets you swap local currency for USDC. To onramp funds in
this way, you must first have a fiat account linked to your Circle Mint account.
This guide walks you through how to use the Cross-Currency API to obtain a quote
for a local currency to USDC exchange and then execute the swap.
## Prerequisites
Before you begin this quickstart, ensure that you have:
* Obtained an API key for Mint from Circle
* Obtained access to the Cross-Currency API
* Installed cURL on your development machine
* Linked a fiat account to your Circle Mint account
This quickstart provides API requests in cURL format, along with example
responses.
## Part 1: Request a quote
To begin an onramp from fiat to USDC, you must first request a quote. Quotes
have a rate guarantee until they expire.
**Note:** [Step 1.1.](#11-get-a-list-of-linked-fiat-accounts) and [Step
1.2.](#12-specify-which-fiat-account-to-use) only need to be performed once.
If you want to switch the fiat account attached to your FX trading account,
you can repeat these two steps.
### 1.1. Get a list of linked fiat accounts
Retrieve a list of the fiat accounts linked to your Circle Mint account using
the following request:
```shell Shell theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/businessAccount/banks/pix' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json JSON theme={null}
{
"data": [
{
"id": "763a80d8-9bbb-4876-a67d-e8089389b016",
"type": "wire",
"status": "complete",
"createDate": "2025-09-16T15:53:47.251Z",
"updateDate": "2025-09-16T15:53:48.293Z",
"description": "Banco Azteca ****7771",
"trackingRef": "CIR3A3R7EV",
"virtualAccountEnabled": true,
"fingerprint": "faf6b92c-4e76-426c-9741-95aed1415715",
"billingDetails": {
"name": "Satoshi Nakamoto",
"line1": "100 Money Street",
"line2": "Suite 1",
"city": "Boston",
"postalCode": "01234",
"district": "MA",
"country": "US",
"valid": true
},
"bankAddress": {
"bankName": "Banco Azteca",
"line1": "Iabel la Católica 165",
"line2": "Colonia Obrera",
"city": "México DF",
"district": "México DF",
"country": "MX"
}
}
]
}
```
### 1.2. Specify which fiat account to use
Register your local fiat account with the Cross-Currency API to make it the
account that settles trades. The following is an example request to register
your fiat account:
```shell Shell theme={null}
curl --location --request PUT 'https://api-sandbox.circle.com/v1/exchange/fxConfigs/accounts' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016",
"currency": "MXN"
}'
```
**Response**
```json JSON theme={null}
{
"data": {
"currency": "MXN",
"fiatAccountId": "763a80d8-9bbb-4876-a67d-e8089389b016",
"createDate": "2025-01-09T17:50:09.452241Z",
"updateDate": "2025-02-28T17:25:15.866900Z"
}
}
```
### 1.3. Request a quote from the API
Using a UUIDv4 generator, generate a UUID to use as the idempotency key. Using
the idempotency key, generate a quote for exchanging a specific amount of local
currency to USDC. You must include an `amount` field on either the `from` or the
`to` object, but not both. The `type` field must be set to `tradable` to get a
locked rate quote. The following is an example request for a quote:
**Note:** Quotes are valid for 3 seconds and must be refreshed if not used in
that time frame.
```shell Shell theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/quotes' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "tradable",
"idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b",
"from": {
"currency": "MXN",
"amount": 100.00
},
"to": {
"currency": "USDC",
"amount": null
}
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1",
"rate": 0.0597,
"from": {
"currency": "MXN",
"amount": 100.0
},
"to": {
"currency": "USDC",
"amount": 5.01
},
"expiry": "2023-10-26T14:37:20.804786Z",
"type": "tradable"
}
}
```
## Part 2: Initiate the trade
Once you have obtained a quote, you can lock in the rate by accepting it and
initiating the trade. If you were building a UI for your users, you would
display the quote to your users and let them confirm the trade.
### 2.1. Create the trade
Generate another idempotency key, and then use it to confirm the quote and lock
in the rate using the following example request:
```shell Shell theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/trades' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6",
"quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1"
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6",
"from": {
"currency": "MXN",
"amount": 100
},
"to": {
"currency": "USDC",
"amount": 5.01
},
"status": "pending",
"createDate": "2023-10-26T14:37:20.804786Z",
"updateDate": "2023-10-26T14:37:20.804786Z",
"quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1"
}
}
```
### 2.2. Check the trade status
After creating a trade, you must check the trade status before sending funds.
Use the following request to retrieve the trade status:
```shell Shell theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "7a6cba1e-6b8d-4bb8-b236-5c20a12e88f6",
"from": {
"currency": "MXN",
"amount": 100
},
"to": {
"currency": "USDC",
"amount": 5.01
},
"status": "confirmed",
"createDate": "2023-10-26T14:37:20.804786Z",
"updateDate": "2023-10-26T14:37:21.123456Z",
"quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1"
}
}
```
Only proceed to send funds when the trade status is `confirmed`. The possible
status values are:
* `pending`: The trade is not yet executed. Do not send funds.
* `confirmed`: The trade is fully executed. You can proceed to send funds.
* `failed`: The trade has failed. Do not send funds.
## Part 3: Send the funds
Once you have confirmed that your trade status is `confirmed`, you can send the
fiat funds to Circle. Do not send funds if the trade status is `pending` or
`failed`. Once received, the USDC is transferred to the appropriate address.
### 3.1. Get settlement batches
Retrieve unsettled payment batches from the API using the following example
request:
```shell Shell theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json JSON theme={null}
{
"data": [
{
"id": "67276b7d-7ea5-4f22-a231-09e2d2891c36",
"entityId": "c5692eb6-33f9-431d-9481-2eee38f02081",
"status": "pending",
"createDate": "2025-09-11T15:45:04.729442Z",
"updateDate": "2025-09-11T15:45:04.729443Z",
"details": [
{
"id": "02bd22dc-b40f-49b8-b2d8-6e69f82cfca0",
"type": "payable",
"status": "pending",
"reference": "ezBrwN2nP5Bz18Lu",
"amount": {
"currency": "MXN",
"amount": "100.00"
},
"expectedPaymentDueAt": "2025-09-11T16:45:00Z",
"createDate": "2025-09-11T15:45:04.728466Z",
"updateDate": "2025-09-11T15:45:04.728466Z"
},
{
"id": "afbd8d53-34ea-42fa-9691-4a5c0dd96c53",
"type": "receivable",
"status": "pending",
"amount": {
"currency": "USDC",
"amount": "5.01"
},
"createDate": "2025-09-11T15:45:04.728479Z",
"updateDate": "2025-09-11T15:45:04.728479Z"
}
]
}
]
}
```
### 3.2. Get payment instructions
Retrieve the settlement instructions for the currency you are using with the
following example request. Note that this step only needs to be performed once,
as the values are static and can be safely cached.
```shell Shell theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements/instructions/MXN' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json JSON theme={null}
{
"data": {
"currency": "MXN",
"fiatAccountType": "wire",
"instruction": {
"beneficiary": {
"name": "PLATAFORMA INTEGRAL DE CRIPTOS Y OTROS A",
"address1": "AVENIDA INSURGENTES SUR 3579",
"address2": "COLONIA VILLA OLÍMPICA, ALCALDÍA TLALPAN"
},
"beneficiaryBank": {
"swiftCode": "AZTKMXMMXXX",
"routingNumber": "322286803",
"accountNumber": "127180987654321012",
"currency": "MXN",
"name": "BANCO AZTECA",
"address": "AVENIDA INSURGENTES SUR 3579",
"postalCode": "14020",
"country": "MX"
},
"trackingRef": "ezBrwN2nP5Bz18Lu"
}
}
}
```
### 3.3. Create a fiat transfer
Initiate an inbound fiat transfer to the account specified in the instructions.
You should include the reference to the settlement batch in the transfer
request. When Circle receives the transfer, Circle updates the settlement batch
and transfers USDC to your Mint account.
If you were building a UI for your users, you would display instructions on how
to complete the transfer. In such a scenario, when the USDC is allocated to your
Mint account, you must then transfer it to the end user that requested the
trade.
**Warning:** You must only send the fiat transfer on the specified payment rail.
In this example, for `MXN`, you must send the transfer on the SPEI payment rail.
Transfers on unsupported payment rails may take significant time and cost to
recover.
# How Stablecoin Payins and Payouts Work
Source: https://developers.circle.com/circle-mint/how-stablecoin-payins-and-payouts-work
Learn how stablecoin payins and payouts work, including payment intents, payin and payout flows, and refunds.
Stablecoin payins and payouts let you move USDC and EURC onchain through Circle
Mint. A stablecoin payin is when your customer sends USDC or EURC to you onchain
and Circle records that transfer. A stablecoin payout is when you send USDC or
EURC to a customer, vendor, or supplier onchain.
## Payment intents
You create a payment intent when a customer chooses to pay with USDC or EURC on
a
[supported blockchain](/circle-mint/references/supported-chains-and-currencies).
The intent carries settlement currency, allowed chains, and the deposit address
your customer uses.
### Continuous and transient modes
In practice, think of the two payment intent modes like this:
* **Continuous** fits repeat deposits. The customer gets a long-lived deposit
address they can use more than once (for example top-ups or billing that
reuses one address). Each successful inbound transfer is recorded as its own
payment.
* **Transient** fits a one-time checkout. You declare a fixed amount for one
purchase or session (for example one cart or invoice). You still create a
payment intent and show a deposit address, but this address cannot be reused
for later top-ups or future orders.
The
[Create payment intent](/api-reference/circle-mint/payments/create-payment-intent)
endpoint reflects that split in the request and response. Continuous omits a
top-level `amount` at creation. Transient requires `type: "transient"` and an
`amount`. The following table compares what you send when you create an intent
and how you should use the deposit address in each mode.
| | Continuous | Transient |
| ------------------ | ---------------------------------------------------- | ------------------------------------------------ |
| Amount at creation | Omitted. You set `currency` and settlement. | You set a fixed `amount`. |
| Deposit address | Same address can receive multiple transfers. | Suited to a single checkout or one-time payment. |
| How you select it | Omit `type` (default) or set `type` to `continuous`. | Set `type` to `transient`. |
When a transfer settles, Circle creates a
[payment](/api-reference/circle-mint/payments/get-payment) for it. For
continuous payment intents, each transfer is represented as its own payment. For
transient payment intents, the deposit address is used for a single checkout or
one-time payment.
## Payin flow
To [receive a stablecoin payin](/circle-mint/receive-stablecoin-payin), the
customer, your customer-facing UI, your server, and Circle follow the same steps
whether the intent is continuous or transient. The customer-facing UI is the
interface your customer uses to make a payment, such as your website or mobile
app. Your server calls Circle Mint APIs and handles webhooks or polling.
On checkout, the customer chooses to pay with stablecoins in your
customer-facing UI. Your server calls the [Create a payment
intent](/api-reference/circle-mint/payments/create-payment-intent) endpoint
with the currency, blockchain, payment intent type, and (for transient only)
amount.
Your server receives the deposit address by using
[webhooks](/circle-mint/circle-apis-notifications-quickstart) or by polling
the [Get a payment
intent](/api-reference/circle-mint/payments/get-payment-intent) endpoint
until `paymentMethods` includes an `address`.
Your customer-facing UI shows the deposit address (and for transient only,
the amount). The customer sends USDC or EURC from their wallet to that
address on the correct blockchain.
Circle detects the transfer. It creates or updates payment data and sends
notifications. Your server treats the payin as complete when payment and
payment intent state match your policies.
The following diagram illustrates the payin flow at checkout:
```mermaid theme={null}
sequenceDiagram
participant C as Customer
participant W as Customer-facing UI
participant S as Your server
participant M as Circle Mint APIs
Note over C,M: Create payment intent
C->>W: Chooses to pay with stablecoins on a blockchain
W->>S: Requests onchain deposit
S->>M: Sends payment intent creation request
M-->>S: Sends payment intent
Note over C,M: Send and display deposit address
M-->>S: Sends notification containing: payment intent status (pending), deposit address
S->>W: Sends deposit address
W->>C: Displays deposit address
Note over C,M: Customer pays onchain and Circle notifies your server
C->>M: Transfers funds to deposit address
M-->>S: Sends notification containing: payment status (paid)
M-->>S: Sends notification containing: payment intent status (complete)
Note over C,M: Confirm payment to customer
S->>W: Sends payment confirmation
W->>C: Displays payment confirmation
```
## Payout flow
To [send a stablecoin payout](/circle-mint/send-stablecoin-payout), your server
calls the Crypto Payouts API. The recipient receives USDC or EURC at the wallet
address you register.
Your server creates a recipient by calling the [Create address book
recipient](/api-reference/circle-mint/payouts/create-address-book-recipient)
endpoint with the destination blockchain and address. The recipient status
must be `active` before continuing.
Your server calls the [Create
payout](/api-reference/circle-mint/payouts/create-payout) endpoint with the
address book recipient ID, `sourceWalletId`, and amounts. The payout starts
in `pending`.
Circle debits your Mint balance and sends funds to the registered address on
the selected blockchain.
Treat the payout as finalized when the `status` is `complete`. Use
[webhooks](/circle-mint/circle-apis-notifications-quickstart) for `payout`
events or poll the [Get
payout](/api-reference/circle-mint/payouts/get-payout) endpoint.
The following diagram illustrates the payout flow:
```mermaid theme={null}
sequenceDiagram
participant S as Your server
participant M as Circle Mint APIs
participant R as Recipient wallet
Note over S,M: Create address book recipient
S->>M: Sends create address book recipient request
M-->>S: Sends address book recipient
Note over S,M: Create payout
S->>M: Sends create payout request
M-->>S: Sends payout (pending)
Note over M,R: Onchain delivery
M->>R: Transfers USDC or EURC to registered address
Note over S,M: Payout complete
M-->>S: Sends notification containing: payout status (complete)
```
## Payment intent expiration
A payment intent can expire. Expiration does not automatically return funds. The
intent mainly tells the customer which deposit address to use. If funds still
arrive at that address, Circle processes it as a payin on your Mint account.
## Refunds
[Issue a full or partial refund](/circle-mint/refund-stablecoin-payin) through
the refund APIs or by signing in to your
[Circle Mint](https://app.circle.com/signin) account.
What you should know about refunds:
* Start a refund after at least one payment on the intent has completed and
settled. You cannot start a refund while a payment is still pending.
* Submit refunds within 30 days of creating the payment intent. During that
period you can send more than one partial refund, up to the limits of your
Mint account.
* After you start a refund, the payment intent status moves to `refunded`. The
intent stops accepting new payins. Don't reuse a refunded intent for new
checkouts. Funds sent to that address might not match the intent and can
require support.
* If you use your Circle Mint account to refund, the refund cannot be canceled
once submitted.
# How-to: Deposit Fiat
Source: https://developers.circle.com/circle-mint/howtos/deposit-fiat
Link a bank account and deposit fiat to mint USDC or EURC in your Circle Mint account.
Deposit fiat (onramp) from an external bank account to mint USDC or EURC in your
Circle Mint balance. The `/wires` endpoint supports multiple payment rails,
including standard wires (FedWire and SWIFT), real-time interbank rails (RTP,
SPEI, SEPA, and CHATS) where available, and book transfers when you bank with
one of Circle's settlement partners.
This guide focuses on standard wire deposits. The same endpoint handles other
rails -- Circle routes the deposit based on your linked bank and region.
Rail-specific parameters and additional endpoints (such as CUBIX and PIX for
local currencies) are out of scope here.
## Prerequisites
Before you begin:
* Complete the
[account and API key setup](/circle-mint/quickstarts/getting-started).
* Have access to a bank account that can send wire transfers.
## Step 1. Link a bank account
Use the
[create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account)
endpoint to register your external bank account with Circle Mint.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/banks/wires \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey":"ba943ff1-ca16-49b2-ba55-1057e70ca5c7","accountNumber":"12340010","routingNumber":"121000248","billingDetails":{"name":"Satoshi Nakamoto","city":"Boston","country":"US","line1":"100 Money Street","district":"MA","postalCode":"01234"},"bankAddress":{"bankName":"SAN FRANCISCO","city":"SAN FRANCISCO","country":"US","line1":"100 Money Street","district":"CA"}}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636",
"status": "pending",
"description": "WELLS FARGO BANK, NA ****0010",
"trackingRef": "CIR2GKYL4B",
"virtualAccountEnabled": true,
"billingDetails": {
"name": "Satoshi Nakamoto",
"line1": "100 Money Street",
"city": "Boston",
"postalCode": "01234",
"district": "MA",
"country": "US"
},
"bankAddress": {
"bankName": "WELLS FARGO BANK, NA",
"line1": "100 Money Street",
"city": "SAN FRANCISCO",
"district": "CA",
"country": "US"
},
"createDate": "2023-11-04T20:02:21.062Z",
"updateDate": "2023-11-04T20:02:21.062Z"
}
}
```
Record the `id` and `trackingRef` values from the response. You use both in the
following steps.
## Step 2. Retrieve wire instructions
Use the
[get wire instructions](/api-reference/circle-mint/account/get-business-wire-account-instructions)
endpoint to fetch the beneficiary details your bank needs to send the wire.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/banks/wires/${BANK_ACCOUNT_ID}/instructions \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": {
"trackingRef": "CIR22FEP33",
"beneficiary": {
"name": "CIRCLE INTERNET FINANCIAL INC",
"address1": "1 MAIN STREET",
"address2": "SUITE 1"
},
"virtualAccountEnabled": true,
"beneficiaryBank": {
"name": "CRYPTO BANK",
"address": "1 MONEY STREET",
"city": "NEW YORK",
"postalCode": "1001",
"country": "US",
"swiftCode": "CRYPTO99",
"routingNumber": "999999999",
"accountNumber": "123815146304",
"currency": "USD"
}
}
}
```
The response includes:
* **Beneficiary details** -- the name and address of the recipient (Circle).
* **Routing information** -- the bank name, SWIFT code, and routing number for
the beneficiary bank.
* **Virtual Account Number** -- the `beneficiaryBank.accountNumber` field,
unique to your linked bank account.
* **Tracking reference** -- the `trackingRef` value to include in the wire memo.
### Virtual account numbers
Not all Circle settlement banks support Virtual Account Numbers. When
`virtualAccountEnabled` is `true` in the wire instructions response, the linked
bank account has a unique VAN in the `beneficiaryBank.accountNumber` field. When
the sender includes the VAN as the account number on their wire, the tracking
reference in the wire memo becomes optional.
When `virtualAccountEnabled` is `false`, the sender must include the
`trackingRef` in the wire memo so Circle can match the deposit to your account.
Benefits of using a VAN (where supported):
* Eliminates the need for senders to include tracking references in payment
instructions.
* Reduces wire returns caused by missing or incorrect tracking references.
* Supports all wire types: domestic, international, and SWIFT.
## Step 3. Send the wire deposit
In production, initiate the wire transfer from your bank using the instructions
returned in Step 2. Confirm that the wire details -- beneficiary name, account
number, and routing number -- match exactly. Mismatched details can result in
wire returns or processing delays of several business days.
Domestic wire deposits received before the daily cutoff typically settle on the
same business day. International wires may take longer depending on intermediary
banks.
### Simulate a deposit in sandbox
In the sandbox environment, use the
[mock wire payment](/api-reference/circle-mint/account/create-mock-wire-payment)
endpoint to simulate a wire deposit without sending real funds. Provide the
`trackingRef` from Step 2 and the `beneficiaryBank.accountNumber` (the VAN) from
the wire instructions response.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/mocks/payments/wire \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"amount":{"amount":"50.00","currency":"USD"},"trackingRef":"CIR22FEP33","beneficiaryBank":{"accountNumber":"123815146304"}}'
```
Expected response:
```json theme={null}
{
"data": {
"trackingRef": "CIR22FEP33",
"amount": {
"amount": "50.00",
"currency": "USD"
},
"beneficiaryBank": {
"accountNumber": "123815146304"
},
"status": "pending"
}
}
```
The mock wire endpoint is available in sandbox only. Mock wire deposits
process in batches and may take up to 15 minutes to complete.
## Step 4. Verify the deposit
Use the
[list deposits](/api-reference/circle-mint/account/list-business-deposits)
endpoint to confirm the incoming fiat deposit has settled.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/deposits \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": [
{
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "1000066041",
"destination": {
"type": "wallet",
"id": "1000066041"
},
"amount": {
"amount": "50.00",
"currency": "USD"
},
"status": "complete",
"createDate": "2024-01-01T12:00:00.000Z"
}
]
}
```
## See also
* [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) --
understand the minting process
* [Sandbox to Production](/circle-mint/references/sandbox-and-testing) --
transition to production
* [Create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account)
\-- API reference
# How-to: Transfer USDC Onchain
Source: https://developers.circle.com/circle-mint/howtos/transfer-on-chain
Receive USDC and EURC via deposit addresses and send them to external blockchain wallets using the Circle Mint API.
Receive USDC or EURC by generating deposit addresses for external wallets to
send to, or send USDC or EURC to allowlisted recipient addresses on supported
blockchains. Transfers of \$3,000 or more require Travel Rule compliance data for
third-party payouts.
## Prerequisites
Before you begin:
* Complete the
[account and API key setup](/circle-mint/quickstarts/getting-started).
* Review
[supported chains and currencies](/circle-mint/references/supported-chains-and-currencies)
for available blockchains.
* (For sending) Have a funded Circle Mint account.
* (For sending) All recipient addresses must be approved by an account
administrator through the [Mint Console](https://app.circle.com/signin) before
you can create transfers. If your account is domiciled in France or Singapore,
addresses require additional verification through the Mint Console.
## Step 1. Receive USDC via deposit address
### Step 1.1. Create a deposit address
Use the create deposit address endpoint to generate an address for receiving
USDC or EURC on a specific blockchain.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/deposit \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "currency": "USD", "chain": "ARC"}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "d51d72d2-9955-4340-b3fd-2f07a82a1e6c",
"address": "0xbd01242af414961c25aa72dcae06646fc52e9b92",
"currency": "USD",
"chain": "ARC"
}
}
```
You can create one deposit address per blockchain. Use the same address for
all deposits on that blockchain.
### Step 1.2. Send funds to your deposit address
This step happens outside the Circle Mint API. The sender transfers USDC or EURC
from their external wallet to your deposit address on the matching blockchain.
Sending funds on the wrong blockchain results in permanent loss. Always
confirm the blockchain network matches between the sender's wallet and your
deposit address.
### Step 1.3. Verify the deposit
Use the
[list transfers](/api-reference/circle-mint/account/list-business-transfers)
endpoint to confirm the incoming transfer has settled.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/transfers \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": [
{
"id": "a6a1b575-13d5-4e73-9da7-73e2a3e4418a",
"source": {
"type": "blockchain",
"chain": "ARC"
},
"destination": {
"type": "wallet",
"id": "1000066041"
},
"amount": {
"amount": "100.00",
"currency": "USD"
},
"transactionHash": "0x4cfd25b5ab46e9fe25e845e7a7e0ea2f1f7e4bba3c6e0f1db0b846e4a1bc5fd2",
"status": "complete",
"createDate": "2024-01-01T12:00:00.000Z"
}
]
}
```
The transfer reaches `complete` status after the required number of
[blockchain confirmations](/circle-mint/references/blockchain-confirmations) for
the deposit's blockchain.
## Step 2. Send USDC to an external address
### Step 2.1. Add a recipient address
Use the create recipient address endpoint to allowlist an external blockchain
address for outbound transfers.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/recipient \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey": "2a308497-e66e-4c42-ac1e-7bedab86d958", "address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1", "chain": "ARC", "currency": "USD", "description": "Treasury wallet"}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "cfa01bb0-d166-5506-a48a-56f2beab559f",
"address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1",
"chain": "ARC",
"currency": "USD",
"description": "Treasury wallet"
}
}
```
Adding a recipient address through the API creates a pending request. An account
administrator must approve the address through the
[Mint Console](https://app.circle.com/signin) before you can send transfers to
it. A confirmation notification is sent to all administrators.
### Step 2.2. Create a transfer
Use the create transfer endpoint to send funds to the approved recipient
address.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/transfers \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey": "6ec3827d-15bb-442e-9d4c-32e73e61cbf4", "destination": {"type": "verified_blockchain", "addressId": "cfa01bb0-d166-5506-a48a-56f2beab559f"}, "amount": {"currency": "USD", "amount": "25.00"}}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea",
"source": {
"type": "wallet",
"id": "1016875042"
},
"destination": {
"type": "blockchain",
"address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1",
"chain": "ARC"
},
"amount": {
"amount": "25.00",
"currency": "USD"
},
"status": "pending",
"createDate": "2024-07-15T16:41:12.395Z"
}
}
```
### Step 2.3. Check the transfer status
Use the get transfer endpoint to monitor the status of your transfer.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/businessAccount/transfers/21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
The transfer progresses through these statuses:
* **`pending`**: Circle received the request.
* **`running`**: The transaction is broadcast onchain. The response includes a
`transactionHash`.
* **`complete`**: Blockchain finality reached -- the required number of
confirmations passed.
Expected response for a running transfer:
```json theme={null}
{
"data": {
"id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea",
"source": {
"type": "wallet",
"id": "1016875042"
},
"destination": {
"type": "blockchain",
"address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1",
"chain": "ARC"
},
"amount": {
"amount": "25.00",
"currency": "USD"
},
"transactionHash": "0x0654eee4f609f9c35e376cef9455dd9fc1546c482c5c32c8f8d434ead14fcf97",
"status": "running",
"createDate": "2024-07-15T16:41:12.395Z"
}
}
```
## Travel Rule compliance
The Financial Crimes Enforcement Network (FinCEN) Travel Rule requires financial
institutions to share originator and beneficiary information for transfers of
\$3,000 or more.
### When it applies
Travel Rule applies to onchain transfers of \$3,000 or more on these blockchains:
* Algorand (ALGO)
* Aptos (APTOS)
* Arbitrum (ARB)
* Avalanche (AVAX)
* Base (BASE)
* Celo (CELO)
* Ethereum (ETH)
* NEAR (NEAR)
* Optimism (OP)
* Polygon PoS (POLY)
* Ripple (XRPL)
* Solana (SOL)
* Stellar (XLM)
### Business account transfers
For transfers from your Circle Mint account using
`POST /v1/businessAccount/transfers`, Circle uses your company identity on file.
No additional data is required in the API request.
### Third-party payouts
For payouts to third parties using the
[Crypto Payouts API](/circle-mint/send-stablecoin-payout) (`POST /v1/payouts`),
you must include the originator's identity in the `source.identities` array.
The following table describes the identities schema:
| Field | Type | Required | Description |
| ------------------------ | ------ | --------------------------- | --------------------------------------- |
| `type` | string | Yes | `individual` or `business` |
| `name` | string | Yes | Full legal name |
| `addresses` | array | No | Array of address objects |
| `addresses[].line1` | string | Yes (if addresses provided) | Street address |
| `addresses[].city` | string | Yes (if addresses provided) | City |
| `addresses[].district` | string | No | State/province (2-letter code for `US`) |
| `addresses[].country` | string | Yes (if addresses provided) | ISO 3166-1 alpha-2 country code |
| `addresses[].postalCode` | string | No | Postal code |
The following example shows a payout request with originator identity data:
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/payouts \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7", "source": {"type": "wallet", "id": "12345", "identities": [{"type": "individual", "name": "Satoshi Nakamoto", "addresses": [{"line1": "100 Money Street", "city": "Boston", "district": "MA", "country": "US", "postalCode": "01234"}]}]}, "destination": {"type": "blockchain", "address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F", "chain": "ETH"}, "amount": {"amount": "3000.00", "currency": "USD"}}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "b36cbf12-6ed1-47ed-9eb9-5874f8991ca8",
"source": {
"type": "wallet",
"id": "12345",
"identities": [
{
"type": "individual",
"name": "Satoshi Nakamoto",
"addresses": [
{
"line1": "100 Money Street",
"city": "Boston",
"district": "MA",
"country": "US",
"postalCode": "01234"
}
]
}
]
},
"destination": {
"type": "blockchain",
"address": "0x8381470ED67C3802402dbbFa0058E8871F017A6F",
"chain": "ETH"
},
"amount": {
"amount": "3000.00",
"currency": "USD"
},
"status": "pending",
"createDate": "2024-07-10T02:13:30.000Z"
}
}
```
### Receiving transfers
If you are a regulated financial institution, Circle provides originator
identity data on inbound transfers of \$3,000 or more. Use
`returnIdentities=true` as a query parameter on GET endpoints:
```bash theme={null}
curl -X GET "https://api-sandbox.circle.com/v1/payouts/{id}?returnIdentities=true" \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
### Failure behavior
If a transfer requires identity data and it is omitted, the transfer fails with:
* `riskEvaluation.decision`: `denied`
* `riskEvaluation.reason`: `3220`
## See also
* [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) --
understand the transfer lifecycle
* [Blockchain Confirmations](/circle-mint/references/blockchain-confirmations)
\-- confirmation counts by blockchain
* [Supported Chains and Currencies](/circle-mint/references/supported-chains-and-currencies)
\-- available blockchains
# How-to: Withdraw Fiat
Source: https://developers.circle.com/circle-mint/howtos/withdraw-fiat
Redeem USDC or EURC to fiat and withdraw funds to your bank account using the Circle Mint API.
Redeem (offramp) USDC or EURC in your Circle Mint balance to fiat and send funds
to a linked bank account. You can track payout status from `pending` through
`complete` or `failed`, and handle returned withdrawals caused by bank-side
rejections. Payouts route through the `/wires` endpoint and support standard
wires, real-time interbank rails (RTP, SPEI, SEPA, CHATS) where available, and
book transfers when applicable -- Circle selects the rail based on your
destination bank and region.
## Prerequisites
Before you begin:
* Complete the
[account and API key setup](/circle-mint/quickstarts/getting-started).
* Have a funded Circle Mint account with available USDC or EURC balance.
* Have a linked bank account. If you have not linked one, see
[Deposit Fiat](/circle-mint/howtos/deposit-fiat) Step 1.
* If your Circle Mint account is domiciled in Singapore or France, verify your
payout recipients through the [Mint Console](https://app.circle.com/signin)
before proceeding. Unverified recipients cause payouts to remain in `pending`
status.
## Step 1. Verify your balance
Before you initiate a withdrawal, confirm that your available balance covers the
amount you plan to send.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/businessAccount/balances \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json"
```
Expected response:
```json theme={null}
{
"data": {
"available": [
{
"amount": "150.00",
"currency": "USD"
}
],
"unsettled": [
{
"amount": "25.00",
"currency": "USD"
}
]
}
}
```
The `available` array shows funds you can withdraw immediately. The `unsettled`
array shows funds that are still being processed and are not yet available.
## Step 2. Create a payout
Use the
[create a payout](/api-reference/circle-mint/account/create-business-payout)
endpoint to send funds from your Circle Mint account to your linked bank
account.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/payouts \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"idempotencyKey": "'$(uuidgen)'", "destination": {"type": "wire", "id": "9d1fa351-b24d-442a-8aa5-e717db1ed636"}, "amount": {"currency": "USD", "amount": "75.00"}}'
```
Replace the `destination.id` value with the bank account ID returned when you
linked your bank account.
Expected response:
```json theme={null}
{
"data": {
"id": "9cf38c76-cac4-40d8-a516-f46e9a610a85",
"amount": {
"amount": "75.00",
"currency": "USD"
},
"status": "pending",
"sourceWalletId": "1016875042",
"destination": {
"type": "wire",
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636",
"name": "WELLS FARGO BANK, NA ****0010"
},
"createDate": "2024-01-15T14:22:31.062Z",
"updateDate": "2024-01-15T14:22:31.062Z"
}
}
```
Record the `id` from the response to check the payout status in the next step.
## Step 3. Check the payout status
Use the [get a payout](/api-reference/circle-mint/account/get-business-payout)
endpoint to check the current status of your withdrawal.
```bash theme={null}
curl -X GET https://api-sandbox.circle.com/v1/businessAccount/payouts/9cf38c76-cac4-40d8-a516-f46e9a610a85 \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json"
```
A payout moves through the following statuses:
* **`pending`**: Circle has received the payout request and is processing it.
* **`complete`**: Funds have been sent to the receiving bank.
* **`failed`**: The payout could not be processed. Check the `errorCode` field
for details.
Expected response for a completed payout:
```json theme={null}
{
"data": {
"id": "9cf38c76-cac4-40d8-a516-f46e9a610a85",
"amount": {
"amount": "75.00",
"currency": "USD"
},
"status": "complete",
"sourceWalletId": "1016875042",
"destination": {
"type": "wire",
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636",
"name": "WELLS FARGO BANK, NA ****0010"
},
"createDate": "2024-01-15T14:22:31.062Z",
"updateDate": "2024-01-16T09:15:42.778Z"
}
}
```
Payouts are asynchronous. To track status changes without polling, subscribe to
`payouts` webhook notifications. Alternatively, poll the get a payout endpoint
at a reasonable interval until the status reaches `complete` or `failed`.
### Returned withdrawals
Even after a payout reaches `complete` status, bank-side issues can cause the
wire to be returned. Common reasons include:
* Incorrect account details, such as a wrong routing number or a closed account.
* Compliance holds at the receiving bank.
* Beneficiary name mismatch between the payout and the bank account on file.
When a wire is returned, the funds are re-credited to your Circle Mint balance.
Monitor webhook notifications for `payouts` events to detect returns. If a
payout fails or is returned, verify the bank account details and retry with a
new idempotency key.
## See also
* [How Minting and Redemption Works](/circle-mint/concepts/how-minting-works) --
understand the redemption process
* [Sandbox to Production](/circle-mint/references/sandbox-and-testing) --
production settlement timing differences
* [Create a payout](/api-reference/circle-mint/account/create-business-payout)
\-- API reference
# Notifications Data Models
Source: https://developers.circle.com/circle-mint/notifications-data-models
Learn about the events and data models used in Circle Notifications.
Circle API notifications are subscriber endpoints that enable you to receive
notifications every time the status of a resource changes.
## Common attributes
All notification messages have the following attributes:
| Name | Type | Description | Sample |
| :----------------- | :---------------- | :---------------------------- | :------------------------------------- |
| `clientId` | `string` (UUIDv4) | Client identifier | `c60d2d5b-203c-45bb-9f6e-93641d40a599` |
| `notificationType` | `string` | The type of notification | `payouts` |
| `version` | `int` | The version of the data model | `1` |
## Implementations
This section lists all notification models.
### Payout
#### Completed
*Completed* payouts are settled payouts. Therefore, the funds should be
available in the destination wallet. The following structure represents the
notifications for completed payouts.
```json Completed Payout Notification Payload theme={null}
{
"clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599",
"notificationType": "payouts",
"payout": {...}
}
```
The `payout` payload is a
[payout object](/circle-mint/circle-api-resources#payout-object).
#### Failed
Failed payouts notifications are structured as follows:
```json Failed Payout Notification Payload theme={null}
{
"clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599",
"notificationType": "payouts",
"payout": {...}
}
```
The `payout` payload is a
[payout object](/circle-mint/circle-api-resources#payout-object).
### Transfer
#### Created
A notification with the structure below is sent on transfer creation.
```json Created Transfer Notification Payload theme={null}
{
"clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599",
"notificationType": "transfers",
"transfer": {...}
}
```
The `transfer` payload is a
[transfer object](/circle-mint/circle-api-resources#transfer-object).
#### Failed
Failed transfers notifications are structured as follows:
```json Failed Transfer Notification Payload theme={null}
{
"clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599",
"notificationType": "transfers",
"transfer": {...}
}
```
The `transfer` payload is a
[transfer object](/circle-mint/circle-api-resources#transfer-object).
#### Completed
Completed transfers notifications are structured as follows:
```json Completed Transfer Notification Payload theme={null}
{
"clientId": "c60d2d5b-203c-45bb-9f6e-93641d40a599",
"notificationType": "transfers",
"transfer": {...}
}
```
The `transfer` payload is a
[transfer object](/circle-mint/circle-api-resources#transfer-object).
# Postman Suite
Source: https://developers.circle.com/circle-mint/postman
Use Circle's Postman collections to easily send API requests and try out our APIs.
*Circle's Postman collection holds sample requests for Circle APIs. Run them in
Postman, an API client. The Circle Mint workspace has one collection per area:
API Overview, Core Functionality, Stablecoin Payins, and Stablecoin Payouts.
Folders follow the same layout as the
[API References](/api-reference/circle-mint/general/ping).*
## How To Use It
### Run in Postman
Select one of the six **Run in Postman** links below. You'll be asked whether
you want to fork the collection to your workspace, view the collection in the
public workspace, or import the collection into Postman.
Fork: Creates a copy of the collection while maintaining a link to the parent.
View: Allows you to quickly try out the API without having to import anything
into your Postman suite. Import: Creates a copy of the collection but does not
maintain a link to Circle's copy.
| Collection | Run it! |
| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| API Overview | [](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-e6b3ea60-0ff0-4919-9a58-eaf262989f82%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799579019\&usg=AOvVaw1J5pjlBnXW56T9Jg1Jem4R) |
| Core Functionality | [](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-3e1635b1-7620-4001-998d-3b3aebbfc44f%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799581882\&usg=AOvVaw2MrbS0b_WrIS2xff7v4UMs) |
| Crypto Deposits API | [](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-27cbac71-8b44-4d50-9c83-7e39a90a7325%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799584756\&usg=AOvVaw1lqKomXCegg8MBf7mppUOm) |
| Crypto Payouts API | [](https://www.google.com/url?q=https://god.gw.postman.com/run-collection/21445022-53206224-cf33-4bbd-8e8a-09d5a780795a?action%3Dcollection%252Ffork%26source%3Drip_markdown%26collection-url%3DentityId%253D21445022-53206224-cf33-4bbd-8e8a-09d5a780795a%2526entityType%253Dcollection%2526workspaceId%253D791ff53c-d236-499c-89ea-307d24ddd289\&sa=D\&source=docs\&ust=1708733799587291\&usg=AOvVaw3tZLn5_kLwqQDkwELUpt3M) |
**Authorization**
To authorize your session authorization, use Circle's Postman variable `apiKey`
and add your API key to the `environment` or `collection` variables. See
Postman's
[using variables](https://learning.postman.com/docs/sending-requests/variables/)
for details.
Need a Circle sandbox API Key? Sign up for a
[Circle account](https://app-sandbox.circle.com/signup)—it only takes a minute
or two.
# Set Up Your Account and API Key
Source: https://developers.circle.com/circle-mint/quickstarts/getting-started
Create a sandbox account, generate an API key, and make your first Circle Mint API request.
This guide walks you through creating a sandbox account, generating an API key,
and verifying that you can connect to the Circle Mint API.
## Prerequisites
Before you begin, ensure you have:
* A valid email address to register for a Circle Mint sandbox account
* [curl](https://curl.se/) or another tool for making HTTP requests
## Step 1: Create a sandbox account
The sandbox environment lets you test API integrations without processing real
transactions. For more details on sandbox versus production environments, see
[Sandbox to Production](/circle-mint/references/sandbox-and-testing).
Go to [app-sandbox.circle.com/signup](https://app-sandbox.circle.com/signup)
and complete the registration form.
Check your inbox and confirm your email to activate your account.
After activation, log in at `https://app-sandbox.circle.com`.
## Step 2: Generate an API key
Circle Mint uses API keys to authenticate all requests. Create and manage keys
in the [Mint Console](https://app-sandbox.circle.com/developer).
Go to
[app-sandbox.circle.com/developer](https://app-sandbox.circle.com/developer).
Restrict where the key can be used from.
API keys grant access to privileged operations on Circle APIs. Store your API
key securely and never expose it in client-side code, public repositories, or
other publicly accessible locations. All API requests must be made over HTTPS.
You can create a maximum of 10 API keys per environment.
## Step 3: Test connectivity
Test raw connectivity by calling the `/ping` endpoint. This endpoint does not
require authentication, so it confirms that your application can reach the API.
```bash theme={null}
curl -s https://api-sandbox.circle.com/ping
```
If your application reached the API, you see the following response:
```json theme={null}
{ "message": "pong" }
```
## Step 4: Verify your API key
Circle Mint uses Bearer token authentication. Include your API key in the
`Authorization` header of every request using the format `Bearer YOUR_API_KEY`.
Call the `/v1/configuration` endpoint to confirm that your API key is valid and
properly configured:
```bash theme={null}
curl -s https://api-sandbox.circle.com/v1/configuration \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
A successful response returns your account configuration, including your
`masterWalletId`. The `masterWalletId` is the identifier for the primary wallet
associated with your Circle Mint account, used as the source or destination in
transfer and payout operations.
```json theme={null}
{ "data": { "payments": { "masterWalletId": "1234567890" } } }
```
If the API key is missing or malformed, you receive a `401 Unauthorized` error:
```json theme={null}
{
"code": 401,
"message": "Malformed authorization. Are the credentials properly encoded?"
}
```
If you see this error, verify that your `Authorization` header uses the `Bearer`
prefix and that you copied the full API key from the Mint Console.
# Quickstart: Mint and Redeem USDC
Source: https://developers.circle.com/circle-mint/quickstarts/mint-and-redeem
Deposit fiat to mint USDC, transfer it onchain, and redeem USDC back to fiat using the Circle Mint API.
This guide walks you through a complete mint-and-redeem cycle in the Circle Mint
sandbox: link a bank account, deposit fiat to mint USDC, transfer USDC onchain,
and redeem USDC back to fiat.
## Prerequisites
Before you begin, complete the
[account and API key setup](/circle-mint/quickstarts/getting-started).
Replace `${YOUR_API_KEY}` in the examples below with your sandbox API key.
## Step 1: Create a bank account
Register a mock bank account using the
[create a wire bank account](/api-reference/circle-mint/account/create-business-wire-account)
endpoint. This bank account serves as the source for depositing fiat and the
destination for redeeming USDC.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/banks/wires \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "unique-id-1",
"accountNumber": "12340010",
"routingNumber": "121000248",
"billingDetails": {
"name": "Satoshi Nakamoto",
"city": "Boston",
"country": "US",
"line1": "100 Money Street",
"district": "MA",
"postalCode": "01234"
},
"bankAddress": {
"bankName": "WELLS FARGO BANK, NA",
"city": "San Francisco",
"country": "US",
"line1": "420 Montgomery Street",
"district": "CA"
}
}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636",
"status": "pending",
"description": "WELLS FARGO BANK, NA ****0010",
"trackingRef": "CIR2GKYL4B",
"fingerprint": "a9a71b77-d83d-4fbc-997f-41a33550c594",
"virtualAccountEnabled": true,
"billingDetails": {
"name": "Satoshi Nakamoto",
"line1": "100 Money Street",
"city": "Boston",
"postalCode": "01234",
"district": "MA",
"country": "US"
},
"bankAddress": {
"bankName": "WELLS FARGO BANK, NA",
"city": "SAN FRANCISCO",
"district": "CA",
"country": "US"
},
"createDate": "2026-01-15T12:00:00.000Z",
"updateDate": "2026-01-15T12:00:00.000Z"
}
}
```
Save the bank account `id` from the response. You need it in the following
steps.
## Step 2: Get wire instructions
Retrieve the wire instructions for your bank account using the
[get wire instructions](/api-reference/circle-mint/account/get-business-wire-account-instructions)
endpoint. The response includes the `trackingRef` and beneficiary
`accountNumber` you need to simulate a wire deposit.
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/banks/wires/9d1fa351-b24d-442a-8aa5-e717db1ed636/instructions \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": {
"trackingRef": "CIR2GKYL4B",
"beneficiary": {
"name": "CIRCLE INTERNET FINANCIAL INC",
"address1": "99 High Street",
"address2": "Boston, MA 02110"
},
"beneficiaryBank": {
"name": "CUSTOMERS BANK",
"routingNumber": "031101279",
"accountNumber": "123815146304",
"city": "Phoenixville",
"postalCode": "19460",
"country": "US"
}
}
}
```
Save the `trackingRef` and the `beneficiaryBank.accountNumber` values for the
next step.
## Step 3: Deposit fiat to mint USDC
Simulate a wire deposit using the
[create a mock wire payment](/api-reference/circle-mint/account/create-mock-wire-payment)
endpoint. In the sandbox, this mints USDC into your Circle Mint account without
moving real funds.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/mocks/payments/wire \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"amount": { "amount": "100.00", "currency": "USD" },
"trackingRef": "CIR2GKYL4B",
"beneficiaryBank": { "accountNumber": "123815146304" }
}'
```
Expected response:
```json theme={null}
{
"data": {
"trackingRef": "CIR2GKYL4B",
"amount": { "amount": "100.00", "currency": "USD" },
"status": "pending"
}
}
```
Sandbox mock wire deposits process in batches and may take up to 15 minutes to
complete. Wait for the deposit to settle before continuing.
After the deposit settles, verify your balance using the
[list all balances](/api-reference/circle-mint/account/list-business-balances)
endpoint:
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/balances \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": {
"available": [{ "amount": "100.00", "currency": "USD" }],
"unsettled": []
}
}
```
The `available` balance confirms that your fiat deposit minted USDC
successfully.
## Step 4: Transfer USDC onchain
Send USDC from your Circle Mint account to an external blockchain address. This
step requires two API calls: create a recipient address, then create a transfer.
### 4.1. Create a recipient address
Register a destination address using the
[create a recipient address](/api-reference/circle-mint/account/create-business-recipient-address)
endpoint. This example uses the Ethereum blockchain.
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/wallets/addresses/recipient \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "unique-id-2",
"address": "0x493A9869E3B5f846f72267ab19B76e9bf99d51b1",
"chain": "ETH",
"currency": "USD",
"description": "External Ethereum wallet"
}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "cfa01bb0-d166-5506-a48a-56f2beab559f",
"address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1",
"chain": "ETH",
"currency": "USD",
"description": "External Ethereum wallet"
}
}
```
### 4.2. Create a transfer
Send USDC to the recipient address using the
[create a transfer](/api-reference/circle-mint/account/create-business-transfer)
endpoint:
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/transfers \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "unique-id-3",
"destination": {
"type": "verified_blockchain",
"addressId": "cfa01bb0-d166-5506-a48a-56f2beab559f"
},
"amount": { "currency": "USD", "amount": "25.00" }
}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "21fd4ec4-bad1-4eb2-9fc5-60320dedc7ea",
"source": { "type": "wallet", "id": "1016875042" },
"destination": {
"type": "blockchain",
"address": "0x493a9869e3b5f846f72267ab19b76e9bf99d51b1",
"chain": "ETH"
},
"amount": { "amount": "25.00", "currency": "USD" },
"status": "pending"
}
}
```
The transfer starts in `pending` status, moves to `running` once broadcast
onchain, and reaches `complete` after enough
[blockchain confirmations](/circle-mint/references/blockchain-confirmations).
## Step 5: Redeem USDC to fiat
Convert your remaining USDC balance back to fiat by creating a payout to the
bank account you registered in Step 1. Use the
[create a payout](/api-reference/circle-mint/account/create-business-payout)
endpoint:
```bash theme={null}
curl -X POST https://api-sandbox.circle.com/v1/businessAccount/payouts \
-H "Authorization: Bearer ${YOUR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "unique-id-4",
"destination": {
"type": "wire",
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636"
},
"amount": { "currency": "USD", "amount": "75.00" }
}'
```
Expected response:
```json theme={null}
{
"data": {
"id": "9cf38c76-cac4-40d8-a516-f46e9a610a85",
"amount": { "amount": "75.00", "currency": "USD" },
"status": "pending",
"sourceWalletId": "1016875042",
"destination": {
"type": "wire",
"id": "9d1fa351-b24d-442a-8aa5-e717db1ed636",
"name": "WELLS FARGO BANK, NA ****0010"
}
}
}
```
## Step 6: Verify the round trip
Check your final balance to confirm both the transfer and payout processed:
```bash theme={null}
curl https://api-sandbox.circle.com/v1/businessAccount/balances \
-H "Authorization: Bearer ${YOUR_API_KEY}"
```
Expected response:
```json theme={null}
{
"data": {
"available": [{ "amount": "0.00", "currency": "USD" }],
"unsettled": []
}
}
```
You completed the full Circle Mint cycle:
1. Deposited \$100 USD via wire to mint 100 USDC.
2. Transferred 25 USDC onchain to an Ethereum address.
3. Redeemed 75 USDC back to fiat via wire payout.
Your available balance returns to zero, confirming every dollar is accounted
for.
The mock wire endpoint used in Step 3 is only available in the sandbox. In
production, initiate wire transfers from your own banking interface using the
wire instructions from Step 2. All other API calls in this guide work the same
in production. See [Sandbox to
Production](/circle-mint/references/sandbox-and-testing) for details on
transitioning.
# Quickstart: Receive a Stablecoin Payin
Source: https://developers.circle.com/circle-mint/receive-stablecoin-payin
Create a payment intent, obtain a deposit address, and confirm onchain USDC or EURC payins using the Crypto Deposits API.
Use the Crypto Deposits API to accept onchain USDC or EURC
[stablecoin payins](/circle-mint/how-stablecoin-payins-and-payouts-work) into
Circle Mint from your customers' wallets. You create the payment intent, share
the deposit address, have the customer send funds, then confirm the transfer
using the API or webhooks. The examples in this quickstart use Ethereum, but you
can use any
[supported blockchain](/circle-mint/references/supported-chains-and-currencies).
## Prerequisites
Before you begin this tutorial, ensure you've:
* Obtained a
[Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with
access to the Crypto Deposits API (stablecoin payins).
* Obtained a `merchantWalletId` for the wallet that receives settled funds.
* Installed cURL for API calls.
* (Optional) Configured
[webhook notifications](/circle-mint/circle-apis-notifications-quickstart).
## Step 1: Create a payment intent
Call
[Create a payment intent](/api-reference/circle-mint/payments/create-payment-intent)
when the customer chooses to pay with USDC or EURC onchain. Choose continuous or
transient mode.
**Expected result:** a response with a payment intent `id` and `timeline`
including `created`.
Continuous payment intents are the default type. You do not include an `amount`
at creation.
Example request:
```bash cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"currency": "USD",
"settlementCurrency": "USD",
"merchantWalletId": "${MERCHANT_WALLET_ID}",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
],
"type": "continuous"
}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
],
"timeline": [
{
"status": "created",
"time": "2023-01-21T20:13:35.579331Z"
}
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-21T20:13:35.578678Z"
}
}
```
Transient intents include a specific `amount` and are intended for a single
checkout payment. You must set the `type` to `transient`.
Example request:
```bash cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"type": "transient",
"amount": {
"amount": "1.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"merchantWalletId": "${MERCHANT_WALLET_ID}",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
]
}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "6e4d4047-db14-4c09-b238-1215aee50d03",
"amount": {
"amount": "1.00",
"currency": "USD"
},
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH"
}
],
"paymentIds": [],
"timeline": [
{
"status": "created",
"time": "2022-07-21T20:13:35.579331Z"
}
],
"createDate": "2022-07-21T20:13:35.578678Z",
"updateDate": "2022-07-21T20:19:24.859052Z"
}
}
```
## Step 2: Obtain the deposit address
Circle does not return the deposit address in the create response. Use webhooks
or poll
[Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent)
until `paymentMethods.address` is set.
**Expected result:** you have the onchain deposit address to show the customer.
After you
[subscribe to notifications](/circle-mint/circle-apis-notifications-quickstart)
for `paymentIntents`, you receive updates when the payment intent changes. When
`paymentMethods.address` is set, the payload includes an updated `timeline` of
the historical payment intent statuses and timestamps.
**Expected result:** the notification includes `paymentIntent.paymentMethods`
with `address` and `timeline` with `pending` after the address is assigned.
Example `paymentIntents` notification after the deposit address is assigned:
```json Continuous theme={null}
{
"clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" },
"paymentIntent": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "0.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }
],
"paymentIds": [],
"refundIds": [],
"timeline": [
{ "status": "pending", "time": "2023-01-21T20:13:38.188286Z" },
{ "status": "created", "time": "2023-01-21T20:13:35.579331Z" }
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-21T20:13:38.186831Z"
}
}
```
```json Transient theme={null}
{
"clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" },
"paymentIntent": {
"id": "6e4d4047-db14-4c09-b238-1215aee50d03",
"amount": { "amount": "1.00", "currency": "USD" },
"amountPaid": { "amount": "0.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }
],
"paymentIds": [],
"refundIds": [],
"timeline": [
{ "status": "pending", "time": "2022-07-21T20:13:38.188286Z" },
{ "status": "created", "time": "2022-07-21T20:13:35.579331Z" }
],
"createDate": "2022-07-21T20:13:35.578678Z",
"updateDate": "2022-07-21T20:13:38.186831Z",
"expiresOn": "2022-07-21T21:13:38.087275Z"
}
}
```
Call the
[Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent)
endpoint until `paymentMethods.address` is present in the response.
**Expected result:** the response `data` includes `paymentMethods` with
`address` for the deposit.
Example request:
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/{id}' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response after the address is available:
```json Continuous theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "0.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }
],
"paymentIds": [],
"refundIds": [],
"timeline": [
{ "status": "pending", "time": "2023-01-21T20:13:38.188286Z" },
{ "status": "created", "time": "2023-01-21T20:13:35.579331Z" }
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-21T20:13:38.186831Z"
}
}
```
```json Transient theme={null}
{
"data": {
"id": "6e4d4047-db14-4c09-b238-1215aee50d03",
"amount": { "amount": "1.00", "currency": "USD" },
"amountPaid": { "amount": "0.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" }
],
"paymentIds": [],
"refundIds": [],
"timeline": [
{ "status": "pending", "time": "2022-07-21T20:13:38.188286Z" },
{ "status": "created", "time": "2022-07-21T20:13:35.579331Z" }
],
"createDate": "2022-07-21T20:13:35.578678Z",
"updateDate": "2022-07-21T20:13:38.186831Z",
"expiresOn": "2022-07-21T21:13:38.087275Z"
}
}
```
## Step 3: Have the customer pay onchain
Display the deposit address and have the customer send USDC or EURC on the
chosen chain.
**Expected result:** an onchain transfer to the deposit address is submitted
(you confirm settlement in Step 4).
### 3.1. Display the deposit address
Give the customer the deposit address, the stablecoin to send (USDC or EURC),
and for transient intents the amount. Plain text or a QR code is fine.
**Expected result:** the customer can send funds from their wallet.
For transient payment intents, you can display the `expiresOn` time to show the
customer how long they have to pay.
If the customer does not send funds before `expiresOn`, the payment intent moves
to a `complete` status with context `expired`. You cannot reuse an expired
intent. Create a new payment intent to retry the checkout.
### 3.2. Customer sends funds onchain
The customer sends USDC or EURC to the deposit address on the specified
blockchain.
**Expected result:** the transfer appears onchain against that address.
## Step 4: Confirm payment completion
After Circle confirms the transfer, it updates the payment intent and creates a
payment record for that inbound transfer. Confirm the intent and the payment
match what you expect using webhooks or polling.
**Expected result:** payment intent `timeline` shows `complete` with context
`paid`, and the linked payment has `status` `paid`.
### 4.1. Inspect the payment intent
Subscribe to `paymentIntents` notifications. After the customer pays, the
payload includes an updated `timeline`, `amountPaid`, and `paymentIds`.
**Expected result:** `paymentIntent` in the notification includes:
* `timeline` with `complete` and context `paid`
* `paymentIds` listing the payin payment for this transfer
* `amountPaid` reflecting the settled amount
Example when the payment intent is complete and paid:
```json Continuous theme={null}
{
"clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" },
"paymentIntent": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" },
{ "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" }
],
"paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2023-01-21T20:19:24.861094Z"
},
{ "status": "pending", "time": "2023-01-21T20:13:38.188286Z" },
{ "status": "created", "time": "2023-01-21T20:13:35.579331Z" }
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-21T20:19:24.859052Z"
}
}
```
```json Transient theme={null}
{
"clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"notificationType": "paymentIntents",
"version": 1,
"customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" },
"paymentIntent": {
"id": "6e4d4047-db14-4c09-b238-1215aee50d03",
"amount": { "amount": "1.00", "currency": "USD" },
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" },
{ "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" }
],
"paymentIds": ["2b8f9d4e-515f-3c3a-a409-8ab99a2e72c7"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2022-07-21T20:19:24.861094Z"
},
{ "status": "pending", "time": "2022-07-21T20:13:38.188286Z" },
{ "status": "created", "time": "2022-07-21T20:13:35.579331Z" }
],
"createDate": "2022-07-21T20:13:35.578678Z",
"updateDate": "2022-07-21T20:19:24.859052Z",
"expiresOn": "2022-07-21T21:13:38.087275Z"
}
}
```
Call the
[Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent)
endpoint on an interval until the newest `timeline` entry has `status` as
`complete` and `context` as `paid`, or until `amountPaid` matches what you
expect.
**Expected result:** the response `data` includes:
* `timeline` with `complete` and context `paid`
* `paymentIds` listing the payin payment for this transfer
* `amountPaid` reflecting the settled amount
Example request:
```curl Continuous theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
```curl Transient theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/6e4d4047-db14-4c09-b238-1215aee50d03' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response when one payin has completed:
```json Continuous theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" },
{ "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" }
],
"paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2023-01-21T20:19:24.861094Z"
},
{ "status": "pending", "time": "2023-01-21T20:13:38.188286Z" },
{ "status": "created", "time": "2023-01-21T20:13:35.579331Z" }
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-21T20:19:24.859052Z"
}
}
```
```json Transient theme={null}
{
"data": {
"id": "6e4d4047-db14-4c09-b238-1215aee50d03",
"amount": { "amount": "1.00", "currency": "USD" },
"amountPaid": { "amount": "1.00", "currency": "USD" },
"amountRefunded": { "amount": "0.00", "currency": "USD" },
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{ "type": "blockchainLeaseFee", "amount": "0.00", "currency": "USD" },
{ "type": "totalPaymentFees", "amount": "0.01", "currency": "USD" }
],
"paymentIds": ["2b8f9d4e-515f-3c3a-a409-8ab99a2e72c7"],
"refundIds": [],
"timeline": [
{
"status": "complete",
"context": "paid",
"time": "2022-07-21T20:19:24.861094Z"
},
{ "status": "pending", "time": "2022-07-21T20:13:38.188286Z" },
{ "status": "created", "time": "2022-07-21T20:13:35.579331Z" }
],
"createDate": "2022-07-21T20:13:35.578678Z",
"updateDate": "2022-07-21T20:19:24.859052Z",
"expiresOn": "2022-07-21T21:13:38.087275Z"
}
}
```
For continuous payment intents, the same deposit address can receive multiple
transfers. Each settled transfer adds another ID to `paymentIds`.
### 4.2. Inspect the payment for the same transfer
The payment record holds the onchain `transactionHash` and the customer's
`fromAddresses` for that transfer.
Subscribe to `payments` notifications to receive the updated `payment` object
when the transfer settles.
**Expected result:** `payment` in the notification has `status` `paid`, a
populated `transactionHash`, and `fromAddresses` for the sender.
```json JSON theme={null}
{
"clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"notificationType": "payments",
"version": 1,
"customAttributes": { "clientId": "f1397191-56e6-42fd-be86-0a7b9bd91522" },
"payment": {
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"fees": { "amount": "0.01", "currency": "USD" },
"createDate": "2023-01-21T20:16:35.092Z",
"updateDate": "2023-01-21T20:19:24.719Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "1.00", "currency": "USD" },
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
}
```
Poll the [Get a payment](/api-reference/circle-mint/payments/get-payment)
endpoint using the ID from `paymentIds` on the
[payment intent](#4-1-inspect-the-payment-intent). Stop when that payment
`status` is `paid` and, if you check onchain, when `transactionHash` matches the
transfer you expect.
For continuous payment intents, `paymentIds` can list more than one transfer
over time. Make sure you poll the ID for this specific transfer.
**Expected result:** `data` has `type` `payment` and `status` `paid`, a
populated `transactionHash`, and `fromAddresses` for the sender when the payin
has settled.
Example request:
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/payments/{id}' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "66c56b6a-fc79-338b-8b94-aacc4f0f18de",
"type": "payment",
"status": "paid",
"amount": { "amount": "1.00", "currency": "USD" },
"fees": { "amount": "0.01", "currency": "USD" },
"createDate": "2023-01-21T20:16:35.092Z",
"updateDate": "2023-01-21T20:19:24.719Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": { "amount": "1.00", "currency": "USD" },
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
},
"transactionHash": "0x7351585460bd657f320b9afa02a52c26d89272d0d10cc29913eb8b28e64fd906"
}
}
```
For blockchains that use a memo or address tag (for example, XLM or HBAR), the
`addressTag` field can appear on `depositAddress` in payment payloads.
The payment is complete when `status` is `paid`.
# Blockchain Confirmations
Source: https://developers.circle.com/circle-mint/references/blockchain-confirmations
Confirmation requirements and approximate times for each blockchain supported by Circle Mint.
## What are blockchain confirmations?
When you submit a transaction to a blockchain, it starts in a pending state. The
network must include it in a block and validate it before it counts as
confirmed. Each new block added after that makes the transaction harder to
reverse.
A **confirmation number** is the number of blocks that must follow a
transaction's block before it is final. Once the confirmation number is reached,
the transaction can't be reversed.
## Why confirmations matter
Without enough confirmations, transactions are at risk of reorganizations
(reorgs). A reorg happens when validators discard recent blocks and replace them
with new ones, rewriting part of the blockchain's history. This can reverse
transactions that appeared settled.
Each extra confirmation makes a reorg less likely. Because blockchains differ in
design, block times, and consensus, the number of confirmations needed varies by
blockchain.
## Confirmation numbers
Confirmations show how safe a transaction is from reorg. The confirmation number
is how many blocks must be added until a block is considered permanent.
Each blockchain's confirmation number is different, and determined by Circle.
Confirmation numbers are based on a variety of factors, including the
blockchain's history, potential for reorg, and overall network architecture.
For layer-2 (L2) blockchains that settle transactions on a separate base layer,
Circle waits for blocks on the layer-1 (L1) base blockchain. This happens after
the L2 block gets included in an L1 block.
| Chain | Confirmations | Approximate time |
| ------------------ | ----------------- | ------------------ |
| Algorand | **1** | \~3 seconds |
| Aptos | **1** | \~500 milliseconds |
| Arbitrum | **12 ETH Blocks** | \~4 to 6 minutes |
| Avalanche C-Chain | **1** | \~2 seconds |
| Base | **12 ETH Blocks** | \~3 to 9 minutes |
| Celo | **12 ETH Blocks** | \~3 to 9 minutes |
| Codex | **12 ETH Blocks** | \~3 to 9 minutes |
| EDGE | **12 ETH Blocks** | \~3 to 5 minutes |
| Ethereum | **12** | \~3 minutes |
| Hedera | N/A | \~3 seconds |
| HyperEVM | **1** | under 1 second |
| Injective | **1** | under 1 second |
| Ink | **12 ETH Blocks** | \~3 to 9 minutes |
| Linea | **65 ETH Blocks** | \~6 to 32 hours |
| Monad | **4** | \~1.6 seconds |
| Morph | **64** | \~20 to 30 minutes |
| NEAR | **1** | \~2 seconds |
| Noble | **1** | \~1.53 seconds |
| OP Mainnet | **12 ETH Blocks** | \~3 to 9 minutes |
| Pharos | **1** | \~20 seconds |
| Plume | **12 ETH Blocks** | \~3 to 9 minutes |
| Polkadot Asset Hub | **1** | \~12 seconds |
| Polygon PoS | **2-3** | \~8 seconds |
| Sei | **1** | \~400 milliseconds |
| Solana | **1** | \~400 milliseconds |
| Sonic | **1** | \~1 second |
| Starknet | **65 ETH Blocks** | \~4 to 8 hours |
| Stellar | **1** | \~5 seconds |
| Sui | **1** | \~500 milliseconds |
| Unichain | **12 ETH Blocks** | \~3 to 9 minutes |
| World Chain | **12 ETH Blocks** | \~3 to 9 minutes |
| XDC | **3** | \~6 seconds |
| XRPL | **1** | \~5 seconds |
| ZKsync Era | **65 ETH Blocks** | \~5 to 7 hours |
**Hedera**: Hedera is built on a hashgraph, not a blockchain. As such, there
isn't a count of confirmations before Circle considers a transfer valid. This
determination is performed on Hedera directly and is then shared back to Circle.
See [Hedera consensus](https://hedera.com/how-it-works) to learn more.
**Linea**: Linea requires 65 ETH block confirmations. However, Linea only posts
batches to Ethereum every 6-32 hours, so the approximate confirmation time
reflects this batch posting interval rather than the ETH block time alone.
## Transfer status
When an incoming transfer is included in a block, the API makes it available for
you. However, the transfer remains in the `running` status. It won't credit the
balance of the associated wallet with the transfer amount until the required
number of confirmations has been reached.
Once the confirmation number is reached, the transfer status changes to
`completed`. If you subscribed to webhook notifications, you receive a message
about this change.
You can use transfers in your processes before they reach `completed` status.
This comes with risk. A blockchain reorganization (reorg) occurs when validators
discard recent blocks and replace them with new ones, reversing transactions
that appeared settled. If a reorg reverses a transfer you already acted on, the
credited balance is rolled back. Reorgs are rare, but if you use transfers
before they get enough confirmations, you take on this risk.
Waiting for confirmations only applies to onchain transfers. These are transfers
where the `source` is type `blockchain`. Transfers between hosted wallets don't
need to wait. These transfers have a `source` type of `wallet` and happen
instantly.
# Sandbox to Production
Source: https://developers.circle.com/circle-mint/references/sandbox-and-testing
Transition your Circle Mint integration from the sandbox environment to production.
Circle provides a sandbox environment at `https://api-sandbox.circle.com` for
prototyping and integration testing. Sandbox APIs match production APIs, so you
can develop and test without generating real financial transactions. Simulated
transactions use
[test networks only](/stablecoins/usdc-contract-addresses#testnet) and do not
move real funds. For details on idempotent requests, pagination, and date
filtering, see the [API reference](/api-reference/idempotent-requests).
After completing your integration in the sandbox, follow these steps to move to
production. If you have questions, contact your Circle solutions engineer or
[customer support](mailto:customer-support@circle.com).
Update all API base URLs from `https://api-sandbox.circle.com` to
`https://api.circle.com`.
Use the [Mint Console](https://app.circle.com/) to create a production
API key.
Production API keys allow you to work with real funds. Treat production
keys with appropriate security protocols. Consult your security team for
key management best practices.
If you configure an IP allowlist, Circle only accepts requests using your
production API key that originate from an IP address on that list. Verify
that the static addresses in your system match the ones you registered with
Circle.
In the sandbox, your API key provides access to all endpoints. In
production, access is restricted to the APIs your entity is authorized to
use.
* Test each API call to confirm it works in production. For example, verify
that your
[list all balances](/api-reference/circle-mint/account/list-business-balances)
call returns results.
* If you receive `403` responses or need additional capabilities, contact
your Circle representative.
Sandbox settlement times are kept short for testing convenience. Production
settlement times reflect real-world processing and are longer. For onchain
transfers, the time to finality depends on the number of
[blockchain confirmations](/circle-mint/references/blockchain-confirmations)
required for each blockchain.
* Test actual settlement times in production.
* Decide whether to perform actions (such as releasing goods) after
confirmation or after settlement.
Sandbox fees may not reflect your client agreement with Circle.
* Review your contract with Circle to understand production fees.
* Test transactions in production to determine actual fees.
* Update your interface if you pass fees along to your customers.
# Supported Chains and Currencies
Source: https://developers.circle.com/circle-mint/references/supported-chains-and-currencies
Blockchains and currencies supported by Circle Mint and related APIs.
**Unsupported assets**
Circle Mint and Circle APIs only support USDC and EURC tokens on the indicated
blockchains. Don't send unsupported tokens such as USDT or bridged USDC to your
Circle Mint address. Doing so might result in a loss of funds.
**Cosmos appchains**
Circle Mint and Circle APIs only support USDC from Noble. If you transfer USDC
from Noble to other appchains via IBC (Inter-Blockchain Communication), you must
transfer it back to Noble before you transfer it to your Circle Mint address.
Don't attempt to deposit USDC from an appchain other than Noble to your Circle
Mint address. Doing so might result in a loss of funds.
**Polkadot parachains**
Circle Mint and Circle APIs only support USDC from Polkadot Asset Hub. If you
transfer USDC from Polkadot Asset Hub to other parachains via XCM, you must
transfer it back to Polkadot Asset Hub before you transfer it to your Circle
Mint address.
Don't attempt to deposit XCM-transferred USDC from a parachain other than
Polkadot Asset Hub to your Circle Mint address. Doing so might result in a loss
of funds.
**Injective**
Circle Mint only supports USDC deposits on Injective through the EVM layer.
Cosmos-layer USDC deposits are not supported. If you deposit USDC through the
Cosmos layer, your funds are stuck and not credited to your Circle Mint account.
To recover stuck funds, contact
[Circle Support](https://help.circle.com/s/submit-ticket).
## USDC
| Chain | API Chain Code | API Currency Code |
| ------------------ | -------------- | ----------------- |
| Algorand | `ALGO` | `USD` |
| Aptos | `APTOS` | `USD` |
| Arbitrum | `ARB` | `USD` |
| Avalanche C-Chain | `AVAX` | `USD` |
| Base | `BASE` | `USD` |
| Celo | `CELO` | `USD` |
| Codex | `CODEX` | `USD` |
| EDGE | `EDGE` | `USD` |
| Ethereum | `ETH` | `USD` |
| Hedera | `HBAR` | `USD` |
| HyperEVM | `HYPEREVM` | `USD` |
| Injective | `INJECTIVE` | `USD` |
| Ink | `INK` | `USD` |
| Linea | `LINEA` | `USD` |
| Monad | `MONAD` | `USD` |
| Morph | `MORPH` | `USD` |
| NEAR | `NEAR` | `USD` |
| Noble | `NOBLE` | `USD` |
| OP Mainnet | `OP` | `USD` |
| Pharos | `PHAROS` | `USD` |
| Plume | `PLUME` | `USD` |
| Polkadot Asset Hub | `PAH` | `USD` |
| Polygon PoS | `POLY` | `USD` |
| Sei | `SEI` | `USD` |
| Solana | `SOL` | `USD` |
| Sonic | `SONIC` | `USD` |
| Starknet | `STRK` | `USD` |
| Stellar | `XLM` | `USD` |
| Sui | `SUI` | `USD` |
| Unichain | `UNI` | `USD` |
| World Chain | `WORLDCHAIN` | `USD` |
| XDC | `XDC` | `USD` |
| XRPL | `XRP` | `USD` |
| ZKsync Era | `ZKS` | `USD` |
## EURC
| Chain | API Chain Code | API Currency Code |
| ----------------- | -------------- | ----------------- |
| Avalanche C-Chain | `AVAX` | `EUR` |
| Base | `BASE` | `EUR` |
| Ethereum | `ETH` | `EUR` |
| Solana | `SOL` | `EUR` |
| Stellar | `XLM` | `EUR` |
| World Chain | `WORLDCHAIN` | `EUR` |
## Crypto Deposits and Payouts APIs
The Crypto Deposits API and Crypto Payouts API support a subset of blockchains
that have USDC available. The following table outlines which blockchain each API
supports.
| Chain | Crypto Deposits API | Crypto Payouts API |
| ------------------ | ------------------- | ------------------ |
| Algorand | ✓ | ✓ |
| Aptos | ✗ | ✗ |
| Arbitrum | ✓ | ✓ |
| Avalanche C-Chain | ✓ | ✓ |
| Base | ✓ | ✓ |
| Celo | ✗ | ✗ |
| Codex | ✓ | ✓ |
| EDGE | ✓ | ✓ |
| Ethereum | ✓ | ✓ |
| Hedera | ✓ | ✓ |
| HyperEVM | ✓ | ✓ |
| Injective | ✓ | ✓ |
| Ink | ✓ | ✓ |
| Linea | ✓ | ✓ |
| Monad | ✓ | ✓ |
| Morph | ✓ | ✓ |
| NEAR | ✗ | ✗ |
| Noble | ✓ | ✓ |
| OP Mainnet | ✓ | ✓ |
| Pharos | ✓ | ✓ |
| Plume | ✓ | ✓ |
| Polkadot Asset Hub | ✗ | ✗ |
| Polygon PoS | ✓ | ✓ |
| Sei | ✓ | ✓ |
| Solana | ✓ | ✓ |
| Sonic | ✓ | ✓ |
| Starknet | ✓ | ✓ |
| Stellar | ✓ | ✓ |
| Sui | ✗ | ✗ |
| Unichain | ✗ | ✗ |
| World Chain | ✓ | ✓ |
| XDC | ✓ | ✓ |
| XRPL | ✓ | ✓ |
| ZKsync Era | ✗ | ✗ |
## Using chains and currencies in the API
Any time you refer to a currency in a Circle Mint API call, you use a currency
and chain pair. For example, to
[create a USDC transfer](/api-reference/circle-mint/account/create-business-transfer)
on Ethereum, specify the `USD` currency on the `ETH` chain.
When referring to balances, you only need to refer to the currency because the
value of the currency for Circle-hosted assets is independent of the chain.
# Supported Countries
Source: https://developers.circle.com/circle-mint/references/supported-countries
Countries that support wire transfers for minting and redeeming USDC.
## Wire transfers for minting and redeeming USDC
The Circle Core API supports wire transfers from and to bank accounts domiciled
in the following countries:
### Asia
* Armenia
* Azerbaijan
* Bahrain
* Bangladesh
* Bhutan
* Bonaire, Sint Eustatius and Saba
* Brunei Darussalam
* Cambodia
* Georgia
* Hong Kong
* Indonesia
* Israel
* Japan
* Jordan
* Kazakhstan
* Kuwait
* Kyrgyzstan
* Malaysia
* Mongolia
* Oman
* Philippines
* Republic of Korea
* Saudi Arabia
* Singapore
* Sri Lanka
* Taiwan
* Tajikistan
* Thailand
* Timor-Leste
* Turkey
* United Arab Emirates
* Uzbekistan
* Vietnam
### Africa
* Angola
* Benin
* Botswana
* British Indian Ocean Territory
* Burkina Faso
* Burundi
* Cameroon
* Chad
* Egypt
* Ethiopia
* The French Southern Territories
* The Gambia
* Ghana
* Kenya
* Madagascar
* Malawi
* Mauritius
* Mayotte
* Morocco
* Mozambique
* Namibia
* Niger
* Réunion
* Rwanda
* Saint Helena, Ascension and Tristan da Cunha
* Senegal
* Seychelles
* South Africa
* Tanzania
* Tunisia
* Western Sahara
* Zambia
* Zimbabwe
### Europe
* Åland Islands
* Andorra
* Austria
* Belgium
* Bosnia and Herzegovina
* Bulgaria
* Croatia
* Cyprus
* Czechia
* Denmark
* Estonia
* The Faroe Islands
* Finland
* France
* Germany
* Gibraltar
* Greece
* Guernsey
* Holy See (Vatican City State)
* Hungary
* Iceland
* Ireland
* Isle of Man
* Italy
* Jersey
* Latvia
* Liechtenstein
* Lithuania
* Luxembourg
* Malta
* The Republic of Moldova
* Monaco
* Montenegro
* Republic of North Macedonia
* Netherlands
* Norway
* Poland
* Portugal
* Romania
* San Marino
* Serbia
* Slovakia
* Slovenia
* Spain
* Svalbard and Jan Mayen
* Sweden
* Switzerland
* United Kingdom
### North America
* Anguilla
* Antigua and Barbuda
* Aruba
* The Bahamas
* Belize
* Bermuda
* British Virgin Islands
* Canada
* Cayman Islands
* Costa Rica
* Curaçao
* Dominica
* The Dominican Republic
* El Salvador
* Greenland
* Grenada
* Guadeloupe
* Honduras
* Martinique
* Mexico
* Montserrat
* Puerto Rico
* Saint Barthelemy
* Saint Lucia
* Saint Martin (French part)
* Saint Pierre and Miquelon
* Saint Vincent and the Grenadines
* Sint Maarten (Dutch part)
* The Turks and Caicos Islands
* United States (excluding HI and NY)
* United States Minor Outlying Islands
* U.S. Virgin Islands
### South America
* Argentina
* Bouvet Island
* Brazil
* Chile
* Colombia
* Ecuador
* French Guiana
* Guatemala
* Guyana
* Paraguay
* Peru
* The Falkland Islands (Malvinas)
* South Georgia and the South Sandwich Islands
* Uruguay
### Oceania
* American Samoa
* Australia
* Christmas Island
* The Cocos (Keeling) Islands
* The Cook Islands
* Federated States of Micronesia
* Fiji
* French Polynesia
* Guam
* Heard Island and McDonald Islands
* Kiribati
* The Marshall Islands
* Nauru
* New Caledonia
* New Zealand
* Niue
* Norfolk Island
* The Northern Mariana Islands
* Palau
* Pitcairn
* Samoa
* Tokelau
* Tuvalu
* Wallis and Futuna
### Antarctica
* Antarctica
This list is subject to change as Circle adds banking partners. Contact the
[Customer Care team](https://support.circle.com/) for the latest information.
# Quickstart: Refund a Stablecoin Payin
Source: https://developers.circle.com/circle-mint/refund-stablecoin-payin
Start full or partial stablecoin payin refunds with the Crypto Deposits API.
Use the Crypto Deposits API to refund USDC or EURC after a completed
[payin](/circle-mint/how-stablecoin-payins-and-payouts-work#refunds), which is a
stablecoin deposit from an external wallet into your Circle Mint account. The
examples use a continuous
[payment intent](/circle-mint/how-stablecoin-payins-and-payouts-work#continuous-and-transient-modes)
(open-ended checkout), but the same steps apply to transient intents (single
checkout with a fixed amount). Complete
[Receive a Stablecoin Payin](/circle-mint/receive-stablecoin-payin) first if you
have not accepted a payin yet.
## Prerequisites
Before you begin this tutorial, ensure you've:
* Obtained a
[Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with
access to the Crypto Deposits API (stablecoin payins).
* [Confirmed a completed stablecoin payin](/circle-mint/receive-stablecoin-payin#step-4-confirm-payment-completion)
on the payment intent you want to refund.
* Confirmed there is no pending payment on that intent.
* Installed cURL for API calls.
## Step 1: Start a refund on the payment intent
Call the
[Refund a payment intent](/api-reference/circle-mint/payments/refund-payment-intent)
endpoint.
* Partial refund: set `toAmount` to the amount you return to the customer. You
can send multiple partial refunds up to what was settled.
* Full refund: use the request fields for refunding the remaining balance as
described in the API reference.
**Expected result:** a `refund` object with `status` `pending` and a refund
`id`.
Example request:
```bash cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b/refund' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "9aed1aab-292a-427f-aae1-e0e358fef1c9",
"destination": {
"chain": "ETH",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"amount": {
"currency": "USD"
},
"toAmount": {
"amount": "0.50",
"currency": "USD"
}
}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6",
"type": "refund",
"status": "pending",
"amount": {
"currency": "USD"
},
"createDate": "2023-01-22T15:29:58.000000Z",
"updateDate": "2023-01-22T15:29:58.000000Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": {
"amount": "0.50",
"currency": "USD"
},
"depositAddress": {
"chain": "ETH",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
}
}
}
```
## Step 2: Inspect the payment intent
Call
[Get a payment intent](/api-reference/circle-mint/payments/get-payment-intent)
to confirm the refund is reflected on the intent.
**Expected result:** the response includes these fields:
* `amountRefunded`: cumulative amount refunded on this intent so far.
* `refundIds`: refund IDs associated with this intent.
* `paymentIds`: IDs of payin payments on this intent.
* `timeline`: includes a `refunded` entry after the refund is recorded
(alongside earlier statuses such as `paid`).
Example request:
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/paymentIntents/e2e90ba3-9d1f-490d-9460-24ac6eb55a1b' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"currency": "USD",
"settlementCurrency": "USD",
"amountPaid": {
"amount": "1.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.50",
"currency": "USD"
},
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0x97de855690955e0da79ce5c1b6804847e7070c7f"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
},
{
"type": "totalPaymentFees",
"amount": "0.01",
"currency": "USD"
}
],
"paymentIds": ["66c56b6a-fc79-338b-8b94-aacc4f0f18de"],
"refundIds": ["3389f4ba-aafd-4eef-aaa2-3292df8f62e6"],
"timeline": [
{
"status": "refunded",
"time": "2023-01-22T15:30:00.000000Z"
},
{
"status": "complete",
"context": "paid",
"time": "2023-01-21T20:19:24.861094Z"
},
{
"status": "pending",
"time": "2023-01-21T20:13:38.188286Z"
},
{
"status": "created",
"time": "2023-01-21T20:13:35.579331Z"
}
],
"type": "continuous",
"createDate": "2023-01-21T20:13:35.578678Z",
"updateDate": "2023-01-22T15:30:00.000000Z"
}
}
```
## Step 3: Confirm refund completion
Call [Get a payment](/api-reference/circle-mint/payments/get-payment) with the
original payin payment `id` or with the refund `id` from
[Step 1](#step-1-start-a-refund-on-the-payment-intent).
**Expected result:** the response has `type` `refund` and `status` `paid` when
the refund has settled onchain.
If `status` is still `pending`, the refund has not finished. Poll
[Get a payment](/api-reference/circle-mint/payments/get-payment) on an interval,
or subscribe to `payments` notifications (see the
[notifications quickstart](/circle-mint/circle-apis-notifications-quickstart))
to learn when the status changes.
Example request: (replace the ID with the payment or refund `id`)
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/payments/3389f4ba-aafd-4eef-aaa2-3292df8f62e6' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "3389f4ba-aafd-4eef-aaa2-3292df8f62e6",
"type": "refund",
"status": "paid",
"amount": {
"currency": "USD"
},
"fees": {
"amount": "0.01",
"currency": "USD"
},
"createDate": "2023-01-22T15:29:58.000000Z",
"updateDate": "2023-01-22T15:35:12.000000Z",
"merchantId": "f1397191-56e6-42fd-be86-0a7b9bd91522",
"merchantWalletId": "1000999922",
"paymentIntentId": "e2e90ba3-9d1f-490d-9460-24ac6eb55a1b",
"settlementAmount": {
"amount": "0.50",
"currency": "USD"
},
"fromAddresses": {
"chain": "ETH",
"addresses": ["0x0d4344cff68f72a5b9abded37ca5862941a62050"]
},
"depositAddress": {
"chain": "ETH",
"address": "0xcd7475eaed9ee9678cf219cec748e25aba068a69"
},
"transactionHash": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678"
}
}
```
# Quickstart: Send a Stablecoin Payout
Source: https://developers.circle.com/circle-mint/send-stablecoin-payout
Create an address book recipient, send USDC or EURC onchain, and confirm payout completion with the Crypto Payouts API.
Use the Crypto Payouts API to send onchain USDC or EURC
[stablecoin payouts](/circle-mint/how-stablecoin-payins-and-payouts-work) from
Circle Mint to third-party blockchain addresses. You create a recipient, wait
until it is active, create a payout, then confirm completion using the API or
webhooks. The examples in this quickstart use Ethereum, but you can use any
[supported blockchain](/circle-mint/references/supported-chains-and-currencies).
Stablecoin payouts are for transferring funds to third-party recipients such as
vendors, contractors, remittances, or customers. If you want to transfer funds
to a wallet your business owns or controls, see the
[Transfer USDC Onchain](/circle-mint/howtos/transfer-on-chain) guide.
## Prerequisites
Before you begin this tutorial, ensure you've:
* Obtained a
[Circle Mint sandbox API key](/circle-mint/quickstarts/getting-started) with
access to the Crypto Payouts API (stablecoin payouts).
* Obtained a source wallet ID (`source.id`) with funds available for payouts.
* Installed cURL for API calls.
* (Optional) Configured
[webhook notifications](/circle-mint/circle-apis-notifications-quickstart).
## Step 1: Create an address book recipient
Add a recipient in your Mint address book before any payout. If delayed
withdrawals are on in the Circle Mint console, new recipients stay `inactive`
for 24 hours before they become `active`. If delayed withdrawals are off,
recipients become `active` within seconds.
Toggle **delayed withdrawals** under your Mint account settings in the Circle
Mint console.
### 1.1. Create a recipient
Call
[Create address book recipient](/api-reference/circle-mint/payouts/create-address-book-recipient).
**Expected result:** a recipient `id` (status may be `pending` until active).
Example request:
```bash cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/addressBook/recipients' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "9352ec9e-5ee6-441f-ab42-186bc71fbdde",
"chain": "ETH",
"address": "0x65BFCf1a6289a0b77b4D3F7d12005a05949FD8C3",
"metadata": {
"email": "satoshi@circle.com",
"bns": "testbns",
"nickname": "test nickname desc"
}
}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "pending",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
### 1.2. Confirm recipient status
The recipient must be `active` before you create a payout. Use webhooks or poll
[Get address book recipient](/api-reference/circle-mint/payouts/get-address-book-recipient).
If delayed withdrawals are on, wait until status moves from `inactive` to
`active`.
**Expected result:** `status` is `active`.
After you
[subscribe to notifications](/circle-mint/circle-apis-notifications-quickstart)
for `addressBookRecipients`, Circle sends updates when a recipient is created or
its status changes.
Example `addressBookRecipients` notification when the recipient becomes active:
```json JSON theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "addressBookRecipients",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"addressBookRecipient": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "active",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
Call the
[Get address book recipient](/api-reference/circle-mint/payouts/get-address-book-recipient)
endpoint on an interval until the recipient `status` is `active`.
Example request: (use the `id` from the
[create recipient](#1-1-create-a-recipient) response)
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/addressBook/recipients/dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response when the recipient is active:
```json JSON theme={null}
{
"data": {
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe",
"chain": "ETH",
"address": "0x65bfcf1a6289a0b77b4d3f7d12005a05949fd8c3",
"metadata": {
"nickname": "test nickname desc",
"email": "satoshi@circle.com",
"bns": "testbns"
},
"status": "active",
"updateDate": "2022-09-22T14:16:34.985353Z",
"createDate": "2022-09-22T14:16:34.985353Z"
}
}
```
Do not continue until the recipient `status` is `active`.
## Step 2: Create a payout
Call [Create payout](/api-reference/circle-mint/payouts/create-payout) with the
recipient `id` from [Create a recipient](#1-1-create-a-recipient), your source
wallet, and amounts. If `toAmount.currency` is omitted, `amount.currency` is
used as the receiving currency.
**Expected result:** a payout `id` with `status` `pending`.
`source.identities` is required for payouts of 3,000 USD or more. It describes
the sender (your organization) for FinCEN
[Travel Rule](/circle-mint/howtos/transfer-on-chain#travel-rule-compliance)
compliance.
Example request:
```bash cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/payouts' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "ba943ff1-ca16-49b2-ba55-1057e70ca5c7",
"source": {
"type": "wallet",
"id": "12345",
"identities": [
{
"type": "individual",
"name": "Satoshi Nakamoto",
"addresses": [
{
"line1": "100 Money Street",
"line2": "Suite 1",
"city": "Boston",
"district": "MA",
"postalCode": "01234",
"country": "US"
}
]
}
]
},
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"currency": "USD"
}
}'
```
Example response:
```json JSON theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"status": "pending",
"updateDate": "2020-04-10T02:13:30.000Z",
"createDate": "2020-04-10T02:13:30.000Z"
}
}
```
If [Get payout](/api-reference/circle-mint/payouts/get-payout) later returns
`status` `failed`, check the payload for an `errorCode` and related fields (for
example insufficient balance or an invalid recipient). Retry only after you fix
the underlying issue.
## Step 3: Confirm payout completion
Confirm the payout using webhooks or by polling
[Get payout](/api-reference/circle-mint/payouts/get-payout).
**Expected result:** `status` is `complete` when funds are sent and delivered
onchain.
Subscribe to `payout` notifications to receive updates as status changes. Each
message includes the payout object with the latest `status`.
Example `payout` notification when the `status` is `complete`:
```json JSON theme={null}
{
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802",
"notificationType": "payout",
"version": 1,
"customAttributes": {
"clientId": "a03a47ff-b0eb-4070-b3df-dc66752cc802"
},
"payout": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2020-04-10T02:13:30.000Z",
"updateDate": "2020-04-10T02:13:30.000Z"
}
}
```
Call the [Get payout](/api-reference/circle-mint/payouts/get-payout) endpoint on
an interval until `status` is `complete`.
Example request: (use the payout `id` from the
[create payout](#step-2-create-a-payout) response)
```bash cURL theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/payouts/b8627ae8-732b-4d25-b947-1df8f4007a29' \
--header 'Accept: application/json' \
--header 'X-Request-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
Example response when the payout is complete:
```json JSON theme={null}
{
"data": {
"id": "b8627ae8-732b-4d25-b947-1df8f4007a29",
"sourceWalletId": "12345",
"destination": {
"type": "address_book",
"id": "dff5fcb3-2e52-5c13-8a66-a5be9c7ecbe"
},
"amount": {
"amount": "3000.14",
"currency": "USD"
},
"toAmount": {
"amount": "3000.14",
"currency": "USD"
},
"fees": {
"amount": "0.00",
"currency": "USD"
},
"networkFees": {
"amount": "0.30",
"currency": "USD"
},
"status": "complete",
"createDate": "2020-04-10T02:13:30.000Z",
"updateDate": "2020-04-10T02:13:30.000Z"
}
}
```
# Quickstart: Swap Between USDC and EURC
Source: https://developers.circle.com/circle-mint/swap-between-usdc-and-eurc
The Cross-Currency API lets you swap between USDC and EURC.
This guide walks you through how to use the Cross-Currency API to obtain a quote
for a EURC to USDC exchange and then execute the swap.
## Prerequisites
Before you begin this quickstart, ensure that you have:
* Obtained an API key for Mint from Circle
* Obtained access to the Cross-Currency API
* cURL installed on your development machine
* Funded your Circle Mint account with EURC
This quickstart provides API requests in cURL format, along with example
responses.
## Part 1: Request a quote
Using a UUIDv4 generator, generate a UUID to use as the idempotency key. Using
the idempotency key, request a quote from the
[`/exchange/quotes`](/api-reference/circle-mint/cross-currency/get-quote)
endpoint for exchanging a specific amount of EURC to USDC. You must include an
`amount` field on either the `from` or the `to` object, but not both. The `type`
field must be set to `tradable` to get a locked rate quote. The following is an
example request for a quote:
**Note:** Quotes are valid for 3 seconds and must be refreshed if not used in
that time frame.
```shell Shell theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/quotes' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "tradable",
"idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b",
"from": {
"currency": "EURC",
"amount": 100
},
"to": {
"currency": "USDC",
"amount": null
}
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1",
"rate": 0.1974,
"from": {
"currency": "EURC",
"amount": 100.0
},
"to": {
"currency": "USDC",
"amount": 110.0
},
"expiry": "2023-10-26T14:37:20.804786Z",
"type": "tradable"
}
}
```
## Part 2: Initiate the trade
Generate another idempotency key, and then use it to confirm the quote with the
[`/exchange/trades`](/api-reference/circle-mint/cross-currency/create-fx-trade)
endpoint and initiate the trade. This locks in the rate. Funds are debited from
your Circle Mint account in accordance with your settlement schedule.
The following is an example request:
```shell Shell theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/exchange/trades' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "07c238ad-b144-4607-9b70-51d1ffbb3c7b",
"quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1"
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1",
"from": {
"currency": "EURC",
"amount": 100.0
},
"to": {
"currency": "USDC",
"amount": 110.0
},
"status": "pending",
"createDate": "2023-10-26T14:37:20.804786Z",
"updateDate": "2023-10-26T14:37:20.804786Z",
"quoteId": "17e1ad29-a223-4ba0-bfb1-cebe861bfed1"
}
}
```
## Part 3: Get the settlement batch (optional)
Once you have initiated the trade, you can optionally retrieve the settlement
batch from the
[`/exchange/trades/settlements`](/api-reference/circle-mint/cross-currency/get-settlements)
endpoint on your account and review the details of the settlement.
```shell Shell theme={null}
curl --location --request GET 'https://api-sandbox.circle.com/v1/exchange/trades/settlements' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}'
```
**Response**
```json JSON theme={null}
{
"data": [
{
"id": "7bf8cf16-2dc1-4514-9b64-c471561a7321",
"entityId": "2bcca31e-784f-4b21-9002-8239551e985f",
"status": "settled",
"createDate": "2025-05-27T19:00:48.688390Z",
"updateDate": "2025-05-27T19:00:51.619761Z",
"details": [
{
"id": "454fc27e-ae8a-461f-834b-d19d8dec481e",
"type": "receivable",
"status": "completed",
"amount": {
"currency": "EURC",
"amount": "1.45"
},
"createDate": "2025-05-27T19:00:48.688333Z",
"updateDate": "2025-05-27T19:00:51.617187Z"
},
{
"id": "d22eebbc-0db9-4818-9f3a-24b39d02524a",
"type": "payable",
"status": "completed",
"amount": {
"currency": "USDC",
"amount": "1.60"
},
"expectedPaymentDueAt": "2025-05-27T22:00:00Z",
"createDate": "2025-05-27T19:00:48.688369Z",
"updateDate": "2025-05-27T19:00:51.617187Z"
}
]
},
{
"id": "e8edbad2-d0a3-4560-b892-d035ca26ba69",
"entityId": "2bcca31e-784f-4b21-9002-8239551e985f",
"status": "settled",
"createDate": "2025-05-19T22:10:29.945750Z",
"updateDate": "2025-05-20T16:52:01.754271Z",
"details": [
{
"id": "b19a182d-ac3d-436b-be4b-5f8b549c74fd",
"type": "payable",
"status": "completed",
"amount": {
"currency": "USDC",
"amount": "1.60"
},
"expectedPaymentDueAt": "2025-05-19T22:00:00Z",
"createDate": "2025-05-19T22:10:29.944724Z",
"updateDate": "2025-05-20T16:52:01.750552Z"
},
{
"id": "6644a7e5-4793-48ef-882b-742cf7bea4ee",
"type": "receivable",
"status": "completed",
"amount": {
"currency": "EURC",
"amount": "1.45"
},
"createDate": "2025-05-19T22:10:29.944441Z",
"updateDate": "2025-05-20T16:52:01.750552Z"
}
]
}
]
}
```
# Manage Webhook Subscriptions
Source: https://developers.circle.com/circle-mint/webhook-subscription-management
Subscribe to webhook notifications to be notified automatically when a transaction occurs.
*Circle's webhooks are an automated method for apps to receive notifications the
moment a transaction is completed or fails to complete. Circle will make a
request to your application when an operation is completed. That means your app
doesn't need to poll Circle to know whether an operation has completed in the
blockchain—the confirmation will arrive automatically.*
For configuring a notification subscriber endpoint, see the
[Notifications Quickstart](/circle-mint/circle-apis-notifications-quickstart).
## Set Up Subscriptions
Through the Circle UI in Sandbox or Production, you can set up webhook
subscriptions. This can be done by:
1. Navigating to Developer → Subscriptions in Sandbox or Production.
2. Selecting Add Subscription
3. Entering an endpoint URL you would like notifications to go to.
4. Selecting Add Endpoint
**Automatic subscription removal** All subscriptions in sandbox are removed after 30 days.
## View Subscriptions
Via Circle's UI you can view all subscriptions registered to your account on
Developer → Subscriptions. Circle's Sandbox environment permits up to three
subscriptions and the Production environment supports one.
## Remove Subscriptions
While viewing subscriptions you are also provided the option to remove
subscriptions. This is done by simply selecting kebab on the Endpoint and
selecting remove.
## API Endpoints
All these actions can also be done using Circle's APIs. See
[API reference subscriptions](/api-reference/circle-mint/general/create-subscription)
for more detail.
# Contracts
Source: https://developers.circle.com/contracts
Circle Contracts empowers developers to utilize smart contracts within their
applications. Contracts facilitates the creation, deployment, and execution of
smart contracts through an intuitive Developer Console or APIs, ensuring
flexibility and ease of integration. The goal is to simplify the process of
leveraging smart contracts for real-world use cases in applications.
Some examples of what developers can do using Contracts:
1. **Deploying Custom Contracts:** Bring your custom contracts to life by
efficiently deploying them onto the blockchain, expanding the functionality
and capabilities of your application.
2. **Deploying NFT Contracts:** Easily deploy NFT contracts and programmatically
mint unique tokens for end-users, enabling the creation of digital assets and
collectibles.
3. **Creating On-Chain Loyalty Programs:** Establish on-chain loyalty programs
within your applications, allowing users to earn and redeem rewards
seamlessly.
4. **Interacting with DeFi Projects:** Effortlessly integrate with popular DeFi
projects like Uniswap, enabling users to interact with decentralized
exchanges and other financial services with just a few clicks.
5. **Integrating Circle Contracts:** Seamlessly incorporate Circle contracts,
such as [CCTP](/cctp/evm-smart-contracts)'s, into your application, providing
secure and efficient transfer functionalities.
6. **Monitor Your Smart Contracts:** Set up push notifications for any events
occurring in your smart contracts.
**No-code & API support** Contracts can be accessed in two ways by developers:
1. **Console:** You can explore, interact and manage smart contracts without
writing any code. This is ideal for non-technical users, or any low frequency
use. The console supports exploring contracts (view functions, events,
transactions), deploying contracts (via templates) and managing the contract
(any read/write function on the contract).
2. **SDK/API:** You can also receive the benefits of Contracts programmatically!
Combining with developer-controlled wallets and Gas Station, you can deploy,
interact and manage smart contracts at scale via simple APIs - in a gasless
fashion! The APIs that we provide include deploying contracts (via bytecode
or templates), read or write contract executions.
## Explore smart contracts
The Developer Services console allows developers to view the details of any
smart contract. Developers can import a smart contract by adding its address and
blockchain. Once imported, developers can explore the contract, view the ABI
functions, read the source code, see all transactions, subscribe to events, or
execute function calls.
## Deploy smart contracts
Developers can deploy smart contracts using Circle Contracts by writing their
contracts or using pre-vetted templates provided by Circle. To deploy a
contract, developers need to create a Developer-controlled wallet using Circle
Wallets and use it to deploy the contract across any supported chains.
Deployment can be done through the console in a no-code way or programmatically
using APIs.
### Deploy a custom contract
If you have already written a smart contract on an IDE, you can deploy it by
providing the compiled bytecode and ABI. For console deployment, create a
console wallet (a smart contract wallet) and use it to deploy the contract on
the desired chain. Include the source code, ABI, and bytecode for API deployment
in the request parameters.
### Deploy contracts with templates
For developers unfamiliar with smart contract engineering, Templates provide
code snippets to deploy contracts without writing any solidity code. These
templates, curated by the Circle team and audited by third-party auditors, cover
popular onchain use cases. Fill in the required properties and deploy the
contract. Templates can be deployed through the console or APIs.
Some templates that we support include:
* Token (ERC-20) contract, by Thirdweb
* NFT (ERC-721) contract, by Thirdweb
* Multi-Token (ERC-1155) contract, by Thirdweb
* Airdrop contract, by Thirdweb
## Manage smart contracts
Once deployed, you can use the console to manage your smart contracts. This
includes viewing analytics, calling contract admin functions (e.g. changing
ownership, updating configs) or subscribing to events. The console provides an
easy way to update and manage contracts post-deployment. Developers can access
analytics such as transactions and events for contracts deployed using Contracts
via APIs.
## Interact with smart contracts
Interacting with smart contracts allows you to integrate existing onchain
contracts into your applications. Import the contract and explore its various
functions. You can add parameters and generate API resources to interact with
the contract in a straightforward manner.
## Event Monitoring
Event Monitoring allows developers to track onchain events emitted by their
smart contracts in real time. By setting up event monitors, you can receive
instant notifications whenever specified events occur. This capability enables
developers to respond programmatically to important actions within their
applications, making it easier to implement automation and enhance user
experiences.
# Airdrop Template
Source: https://developers.circle.com/contracts/airdrop
The Airdrop template is an audited, ready-to-deploy, airdrop smart contract for
ERC-20, ERC-721, ERC-1155, or native tokens. The airdrop is performed by
“pushing tokens,” which happens when the contract owner transfers tokens to the
receivers' addresses. Receivers do not need to take any action and do not incur
a gas fee.
The following are common use cases for the Airdrop template:
* **Bulk token distribution**: Automatically distribute tokens to users in bulk.
For example, project teams or token issuers can reward their community
members, early adopters, and participants in specific campaigns by
distributing tokens directly to their blockchain addresses.
* **Marketing and promotion**: Increase awareness and engagement with a project
or token. When a project distributes tokens to a large number of users, this
attracts attention and incentivizes users to explore and participate in the
project's ecosystem.
* **Community building and engagement**: Foster a strong and active community
around a project. Distribute tokens to community members to incentivize them
to stay engaged, participate in discussions, provide feedback, and contribute
to the overall growth and success of the project.
We recommend setting a maximum of 500 recipients per airdrop transaction. More
than 500 recipients can cause the blockchain fees to spike or the transaction
to fail.
## Deployment parameters
The Airdrop template creates a customized airdrop smart contract to distribute
ERC-20, ERC-721, ERC-1155, or native tokens to multiple users. To create a
contract using this template, provide the following parameter values when
deploying a smart contract template. To deploy a template, send a `POST` request
to the `/templates/{id}/deploy` endpoint.
**Template ID**: 13e322f2-18dc-4f57-8eed-4bddfc50f85e
### Template deployment parameters
| Parameter | Type | Required | Description |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultAdmin` | Add | X | Address of the default admin. This address can execute permissable functions on the contract.
**Important:** You lose administrative access to the contract if this is not set to an address you control. |
| `contractURI` | String | | URL for the marketplace metadata of your contract. |
## Common functions
This section lists the most commonly used functions on the Airdrop template,
their respective parameters, and potential failure scenarios. These functions
include:
* [setOwner \[write\]](#setowner-write)
* [airdropERC1155 \[write\]](#airdroperc1155-write)
* [airdropERC721 \[write\]](#airdroperc721-write)
* [airdropERC20 \[write\]](#airdroperc20-write)
* [owner \[read\]](#owner-read)
## setOwner \[write]
The `setOwner` function sets the owner of the smart contract or transfers
ownership from the existing owner to a new owner.
### Parameters
The name of the parameter is not used in the request body.
| Parameter | Type | Description |
| :----------- | :------ | :------------------------ |
| \_*newOwner* | address | Address of the new owner. |
### Failure scenarios
* The `setOwner` function fails if it is called by a non-admin.
## airdropERC1155 \[write]
The `airdropERC1155` function allows the owner of this address to send ERC-1155
tokens to a list of recipients.
### Parameters
The names of the parameters are not used in the request body.
| Parameter | Type | Description |
| ------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| \_*newOwner* | address | Address of the new owner. |
| \_*contents* | tuple:
- Address that is to receive the airdrop - ID of the token within the ERC-1155 contract to be distributed - Amount of the token within the ERC-1155 contract to be distributed |
### Failure scenarios
The `airdropERC1155` function fails if:
* It is called by a non-admin
* It contains incorrect token information, such as an invalid token ID
* The Airdrop contract is not approved for what's being transferred, which means
it is not authorized to transfer assets on the airdropper's behalf
* Airdropping to a contract without a receive function
### Example
The following sample code shows the new owner address and contents when sending
a `POST` request to the `/developer/transactions/contractExecution` endpoint:
```javascript JSON theme={null}
"abiParameters": [
"0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF",
[
["0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528", 0, 10],
["0xf4e2B0fcbd0DC4b326d8A52B718A7bb43BdBd072", 0, 10],
]
]
```
## airdropERC721 \[write]
The `airdropERC721` function allows the owner of this address to send ERC-20
tokens to a list of recipients.
### Parameters
The names of the parameters are not used in the request body.
| Parameter | Type | Description |
| ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| \_*tokenAddress* | address | Address of the new owner. |
| \_*contents* | tuple:
- address - uint256 | Array of recipient information:
- Address that is to receive the airdrop - Amount of the token within the ERC-721 contract to be distributed |
### Failure scenarios
The `airdropERC721` function fails if:
* It is called by a non-admin
* It contains incorrect token information, such as an invalid token ID
* The Airdrop contract is not approved for what's being transferred, which means
it is not authorized to transfer assets on the airdropper's behalf
* Airdropping to a contract without a receive function
### Example
The following sample code shows the token address and contents when sending a
`POST` request to the `/developer/transactions/contractExecution` endpoint:
```javascript JSON theme={null}
"abiParameters": [
"0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF",
[
["0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528", 0],
["0xf4e2B0fcbd0DC4b326d8A52B718A7bb43BdBd072", 1],
]
]
```
## airdropERC20 \[write]
The `airdropERC20` function allows the owner of this address to send NFTs to a
list of recipients.
### Parameters
The names of the parameters are not used in the request body.
| Parameter | Type | Description |
| ---------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| \_*tokenAddress* | address | Address of the token. |
| \_*contents* | tuple:
- address - uint256 | Array of recipient information:
- Address that is to receive the airdrop - Quantity of tokens to be transferred |
### Failure scenarios
The `airdropERC20` function fails if:
* It is called by a non-admin
* The Airdrop contract is not approved for what's being transferred, which means
it is not authorized to transfer assets on the airdropper's behalf
* Airdropping to a contract without a receive function
### Example
The following sample code shows the token address and contents when sending a
`POST` request to the `/developer/transactions/contractExecution` endpoint:
```javascript JSON theme={null}
"abiParameters": [
"0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF",
"0xd41c057fd1c78805AAC12B0A94a405c0461A6FBb",
[
[
"0x4CCeBa2d7D2B4fdcE4304d3e09a1fea9fbEb1528",
100
]
]
]
```
## owner \[read]
The `owner` function retrieves the address of the current owner.
### Example
The following sample code shows the `owner` function when sending a `POST`
request to the `/contracts/query` endpoint:
```javascript JSON theme={null}
"abiFunctionSignature": "owner()"
```
# How-to: Create an API Key
Source: https://developers.circle.com/contracts/create-api-key
Create an API key in the Circle Console to authenticate Smart Contract Platform API requests.
## Overview
Create an API key so your server-side applications can authenticate requests to
Circle's APIs. You can create and manage API keys in the
[Circle Console](https://console.circle.com/).
## Prerequisites
Before you begin, ensure you have:
* Signed up for a Circle Developer account at
[console.circle.com/signup](https://console.circle.com/signup).
* Reviewed the [Keys](/build/keys) page for background on key types and
authentication.
## Steps
### Step 1. Open the API & Client Keys page
Sign in to the [Circle Console](https://console.circle.com/) and select **API &
Client Keys** from the left sidebar.
### Step 2. Create a key
Select **Create a key**, then choose **API Key**.
Enter a name for your API key and select the access level:
* **Standard**: grants read/write access to all APIs, including newly introduced
endpoints.
* **Restricted Access**: limits the key to specific products and permission
levels.
When you choose restricted access, configure the following options.
Select the products the key can access:
* **Webhooks**: endpoints for
[webhook subscriptions](/api-reference/wallets/common/create-subscription).
* **Wallets**: all endpoints for
[user-controlled](/api-reference/wallets/user-controlled-wallets/create-user)
and developer-controlled wallets.
* **Contracts**: endpoints for
[smart contracts](/api-reference/contracts/smart-contract-platform/import-contract).
Set the permission level for each product:
* **No permission**: the key cannot call any endpoints for that product.
* **Read**: the key can call read-only GET endpoints.
* **Read/Write**: the key can call all endpoints.
You can also add IP addresses or ranges to the IP allowlist for additional
security.
### Step 3. Copy your API key
After the Console confirms that your key was generated, select **Show** to
reveal the key value. Copy it and store it securely. You need it to authenticate
API requests.
# Quickstart: Deploy an ERC-1155 Contract Template
Source: https://developers.circle.com/contracts/deploy-smart-contract-template
Use Circle Contract Templates to deploy smart contracts without writing Solidity
This quickstart walks you through deploying an ERC-1155 Multi-Token contract
using Contract Templates and minting your first token.
Contract Templates make it easy to integrate smart contracts into your
application without writing Solidity code. Deploy contracts in minutes using
curated and audited templates that support popular onchain use cases.
**Note:** This quickstart provides all the code you need to deploy an ERC-1155
contract and mint tokens. You can deploy using either the
[Console](#console-path) or [API](#api-path).
## Prerequisites
Before you begin, ensure you have:
* A [Circle Developer Account](https://console.circle.com)
* For the API path:
* An [API key](/contracts/create-api-key)
* A [dev-controlled wallet](/wallets/dev-controlled/create-your-first-wallet)
* Your
[Entity Secret registered](/wallets/dev-controlled/register-entity-secret)
## Evaluate templates
To learn more about the ERC-1155 template or other templates, visit:
* **[Console](https://console.circle.com):** View templates, their use cases,
ABI functions, events, and code.
* **[Templates Glossary](/contracts/scp-templates-overview):** Review all
templates and their configuration options.
***
## Console path
Use the Console and a Console Wallet to deploy a smart contract template and
mint a token. This is the preferred method for those new to smart contracts.
### Step 1: Set up your Console Wallet
Console Wallets are Smart Contract Accounts designed for use within the Console.
They leverage [Gas Station](/wallets/gas-station), eliminating the need to
maintain gas for transaction fees.
If you don't have a Console Wallet, you'll be prompted to create one during
deployment.
**Console Wallet Deploy Cost:** Unlike EOAs, SCAs cost gas to deploy. With
lazy deployment, you won't pay the gas fee at wallet creation as it's charged
when you initiate your first outbound transaction.
### Step 2: Deploy the smart contract
In the [Console](https://console.circle.com):
1. Navigate to the **Templates** tab.
2. Select **Multi-Token** ERC-1155.
3. Fill in the deployment parameters:
| Parameter | Description |
| :------------------------- | :----------------------------------------------------------------------------------------------------- |
| **Name** | The offchain name of the contract, only visible in Circle's systems. Use `MyERC1155Contract`. |
| **Contract Name** | The onchain name for the contract. Use `MyERC1155Contract`. |
| **Default Admin** | The address with admin permissions to execute permissioned functions. Use your Console Wallet address. |
| **Primary Sale Recipient** | The address that receives first-time sale proceeds. Use your Console Wallet address. |
| **Royalty Recipient** | The address that receives royalties from secondary sales. Use your Console Wallet address. |
| **Royalty Percent** | The royalty share as a decimal (for example, `0.05` for 5% of secondary sales). Use `0`. |
| **Network** | The blockchain network to deploy onto. Select `Arc Testnet`. |
| **Select Wallet** | The wallet to deploy the smart contract from. Select your Console Wallet. |
| **Deployment Speed** | The fee level affecting transaction processing speed (FAST, AVERAGE, SLOW). Select `AVERAGE`. |
4. Select **Deploy**.
**Console Wallet Creation:** After selecting a network, you'll be prompted to
create a Console Wallet. This wallet is automatically created on all available
networks. On testnet, a [Gas Station Policy](/wallets/gas-station/policy-management) is also created.
Once deployed, you'll return to the **Contracts** dashboard. The deployment
status will initially show **Pending**, then change to **Complete** after a few
seconds.
### Step 3: Mint a token
In the [Console](https://console.circle.com):
1. Navigate to the **Contracts** tab.
2. Select your **MyERC1155Contract**.
3. Select the **ABI Functions** tab → **Write** → **mintTo**.
4. Fill in the parameters:
| Parameter | Description |
| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **\_to** | The wallet address to receive the minted token. Use your Console Wallet address. |
| **\_tokenId** | The token ID to mint, identifying the token type in ERC-1155. Use max uint256 (`115792089237316195423570985008687907853269984665640564039457584007913129639935`) to create token ID 0. For subsequent tokens, use `0` for ID 1, `1` for ID 2, etc. |
| **\_uri** | The URI for the token metadata, such as an IPFS CID or CDN URL. |
| **\_amount** | The quantity of tokens to mint. Use `1`. |
5. Select **Execute Function** → ensure your Console Wallet is selected →
**Execute**.
Select **View Transaction History** to monitor the transaction. Once the state
shows **Complete**, the token has been minted successfully.
**Inbound Transaction:** You'll also see an inbound transfer indicating the
token was minted to your Console Wallet.
***
## API path
Use APIs to deploy a smart contract template and mint a token programmatically.
This option requires an API key and a Dev-Controlled Wallet.
### Step 1: Set up your environment
#### 1.1. Get your wallet information
Retrieve your wallet ID using the
[`GET /wallets`](/api-reference/wallets/developer-controlled-wallets/get-wallets)
API. Ensure:
* Wallet custody type is **Dev-Controlled**
* Blockchain is **Arc Testnet**
* Account type is **SCA** (recommended—removes need for gas)
Note your wallet's address for subsequent steps.
#### 1.2. Understand deployment parameters
| Parameter | Description |
| :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
| `idempotencyKey` | A unique value for request deduplication. |
| `name` | The offchain contract name. Use `MyERC1155Contract`. |
| `walletId` | The ID of the wallet deploying the contract. Use your dev-controlled wallet ID. |
| `templateId` | The template identifier. Use `aea21da6-0aa2-4971-9a1a-5098842b1248` for ERC-1155. See [Templates](/contracts/scp-templates-overview). |
| `blockchain` | The network to deploy onto. Use `ARC-TESTNET`. |
| `entitySecretCiphertext` | The re-encrypted entity secret. See [How the Entity Secret Works](/wallets/dev-controlled/entity-secret-management). |
| `feeLevel` | The fee level for transaction processing. Use `MEDIUM`. |
| `templateParameters` | The onchain initialization parameters (see below). |
#### 1.3. Template parameters
| Parameter | Description |
| :--------------------- | :-------------------------------------------------------------------------------- |
| `name` | The onchain contract name. Use `MyERC1155Contract`. |
| `defaultAdmin` | The address with admin permissions. Use your Dev-Controlled Wallet address. |
| `primarySaleRecipient` | The address for first-time sale proceeds. Use your Dev-Controlled Wallet address. |
| `royaltyRecipient` | The address for secondary sale royalties. Use your Dev-Controlled Wallet address. |
| `royaltyPercent` | The royalty share as a decimal (for example, `0.05` for 5%). Use `0`. |
### Step 2: Deploy the smart contract
Deploy by making a request to
[`POST /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template):
```javascript Node.js theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform";
const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({
apiKey: "",
entitySecret: "",
});
const circleContractSdk = initiateSmartContractPlatformClient({
apiKey: "",
entitySecret: "",
});
const response = await circleContractSdk.deployContractTemplate({
id: "aea21da6-0aa2-4971-9a1a-5098842b1248",
blockchain: "ARC-TESTNET",
name: "MyERC1155Contract",
walletId: "",
templateParameters: {
name: "MyERC1155Contract",
defaultAdmin: "",
primarySaleRecipient: "",
royaltyRecipient: "",
royaltyPercent: 0,
},
fee: {
type: "level",
config: {
feeLevel: "MEDIUM",
},
},
});
```
```python Python theme={null}
from circle.web3 import utils, developer_controlled_wallets, smart_contract_platform
client = utils.init_developer_controlled_wallets_client(
api_key="",
entity_secret=""
)
scpClient = utils.init_smart_contract_platform_client(
api_key="",
entity_secret=""
)
api_instance = smart_contract_platform.TemplatesApi(scpClient)
request = smart_contract_platform.TemplateContractDeploymentRequest.from_dict({
"blockchain": "ARC-TESTNET",
"name": "MyERC1155Contract",
"walletId": "",
"templateParameters": {
"name": "MyERC1155Contract",
"defaultAdmin": "",
"primarySaleRecipient": "",
"royaltyRecipient": "",
"royaltyPercent": "0",
},
"feeLevel": "MEDIUM"
})
request.template_parameters["royaltyPercent"] = 0
response = api_instance.deploy_contract_template("aea21da6-0aa2-4971-9a1a-5098842b1248", request)
```
```shell cURL theme={null}
curl --request POST \
--url 'https://api.circle.com/v1/w3s/templates/aea21da6-0aa2-4971-9a1a-5098842b1248/deploy' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'authorization: Bearer ' \
--data '{
"idempotencyKey": "",
"blockchain": "ARC-TESTNET",
"name": "MyERC1155Contract",
"walletId": "",
"templateParameters": {
"name": "MyERC1155Contract",
"defaultAdmin": "",
"primarySaleRecipient": "",
"royaltyRecipient": "",
"royaltyPercent": 0
},
"feeLevel": "MEDIUM",
"entitySecretCiphertext": ""
}'
```
**Response:**
```json theme={null}
{
"data": {
"contractIds": ["b7c35372-ce69-4ccd-bfaa-504c14634f0d"],
"transactionId": "601a0815-f749-41d8-b193-22cadd2a8977"
}
}
```
A successful response indicates deployment has been **initiated**, not
completed. Use the `transactionId` to check status.
#### 2.1. Check deployment status
Verify deployment with
[`GET /transactions/{id}`](/api-reference/wallets/developer-controlled-wallets/get-transaction):
```javascript Node.js theme={null}
const response = await circleDeveloperSdk.getTransaction({
id: "601a0815-f749-41d8-b193-22cadd2a8977",
});
```
```python Python theme={null}
api_instance = developer_controlled_wallets.TransactionsApi(client)
response = api_instance.get_transaction(id="601a0815-f749-41d8-b193-22cadd2a8977")
```
```shell cURL theme={null}
curl --request GET \
--url 'https://api.circle.com/v1/w3s/transactions/601a0815-f749-41d8-b193-22cadd2a8977' \
--header 'accept: application/json' \
--header 'authorization: Bearer '
```
**Response:**
```json theme={null}
{
"data": {
"transaction": {
"id": "601a0815-f749-41d8-b193-22cadd2a8977",
"blockchain": "ARC-TESTNET",
"state": "COMPLETE"
}
}
}
```
### Step 3: Mint a token
Use the `mintTo` function to mint tokens. The wallet must have `MINTER_ROLE`.
```javascript Node.js theme={null}
const response = await circleDeveloperSdk.createContractExecutionTransaction({
walletId: "",
abiFunctionSignature: "mintTo(address,uint256,string,uint256)",
abiParameters: [
"",
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
"ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei",
"1",
],
contractAddress: "",
fee: {
type: "level",
config: {
feeLevel: "MEDIUM",
},
},
});
```
```python Python theme={null}
api_instance = developer_controlled_wallets.TransactionsApi(client)
request = developer_controlled_wallets.CreateContractExecutionTransactionForDeveloperRequest.from_dict({
"walletId": "",
"abiFunctionSignature": "mintTo(address,uint256,string,uint256)",
"abiParameters": [
"",
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
"ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei",
"1"
],
"contractAddress": "",
"feeLevel": "MEDIUM",
})
response = api_instance.create_developer_transaction_contract_execution(request)
```
```shell cURL theme={null}
curl --request POST \
--url 'https://api.circle.com/v1/w3s/developer/transactions/contractExecution' \
--header 'authorization: Bearer ' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{
"abiFunctionSignature": "mintTo(address,uint256,string,uint256)",
"abiParameters": [
"",
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
"ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei",
"1"
],
"idempotencyKey": "",
"contractAddress": "",
"feeLevel": "MEDIUM",
"walletId": "",
"entitySecretCiphertext": ""
}'
```
**Response:**
```json theme={null}
{
"data": {
"id": "601a0815-f749-41d8-b193-22cadd2a8977",
"state": "INITIATED"
}
}
```
Check the transaction status using
[`GET /transactions/{id}`](/api-reference/wallets/developer-controlled-wallets/get-transaction)
as shown above.
***
## Summary
After completing this quickstart, you've successfully:
* Deployed an ERC-1155 Multi-Token contract on Arc Testnet
* Minted your first token using either the Console or API
## Next steps
* Explore the [Templates Glossary](/contracts/scp-templates-overview) for other
contract templates
* Learn about [Gas Station](/wallets/gas-station) for sponsoring transactions
* View your contract on the [Arc Testnet Explorer](https://testnet.arcscan.app/)
# Multi-Token Template
Source: https://developers.circle.com/contracts/erc-1155-multi-token
The Multi-Token template is an audited, ready-to-deploy smart contract for the
ERC-1155 multi-token standard. ERC-1155 is a versatile token standard that
allows for creating and managing multiple types of tokens within a single smart
contract. Unlike other token standards, like ERC-20 and ERC-721, ERC-1155
supports fungible and non-fungible tokens, providing flexibility for various use
cases.
The ERC-1155 standard enables the creation of tokens representing different
types of assets, such as digital collectibles, in-game items, unique artwork,
and more, all within the same contract. This reduces the need to deploy separate
contracts for different token types, improving efficiency and reducing costs.
Some use cases of the standard include:
* **Gaming assets:** With ERC-1155, developers can create game assets that can
be fungible or non-fungible. For example, fungible ERC-1155 tokens can
represent in-game currencies, while non-fungible ERC-1155 tokens can represent
unique weapons, characters, or virtual land.
* **Digital collectibles:** Similar to ERC-721, ERC-1155 can be used to create
and trade digital collectibles. However, ERC-1155 offers additional
flexibility, allowing for the creation of fungible and non-fungible tokens
under the same contract. This enables the creation of collections with varying
levels of scarcity and uniqueness.
* **Tokenized real-world assets:** ERC-1155 tokens can also represent ownership
of real-world assets such as real estate or shares in a company. By combining
fungible and non-fungible tokens, ERC-1155 offers a more efficient solution
for fractional ownership of assets.
* **Batch operations:** One of the significant advantages of ERC-1155 is the
ability to perform batch operations. Developers can transfer multiple tokens
in a single transaction, making it more cost-efficient and reducing gas fees.
In this comprehensive guide, you explore the Multi-Token template, which
provides all the necessary information to deploy and understand the contract's
common functions.
## Deployment parameters
The Multi-Token template creates a smart contract representing and controlling
any number of token types. These tokens can of the ERC-20, ERC-721 or any other
standard. To create a contract using this template, provide the following
parameter values when deploying a smart contract template using the
[`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
API.
**Template ID:** aea21da6-0aa2-4971-9a1a-5098842b1248
### Template deployment parameters
| Parameter | Type | Required | Description |
| ---------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | String | X | Name of the contract - stored on-chain. |
| `symbol` | String | | Symbol of the token - stored onchain. The symbol is usually 3 or 4 characters in length. |
| `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract. |
| `primarySaleRecipient` | String | X | The recipient address for first-time sales. |
| `platformFeeRecipient` | String | | The recipient address for all sale fees. |
| `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to platformFeeRecipient. |
| `royaltyRecipient` | String | X | The recipient address for all royalties (secondary sales). This allows the contract creator to benefit from further sales of the contract token. |
| `royaltyPercent` | Float | X | The percentage of secondary sales that go to the royalty recipient. For example, set it as 0.05 if you want royalties to be 5% of secondary sales. |
| `contractUri` | String | | The URL for the marketplace metadata of your contract. |
| `trustedForwarders` | String\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. |
Here is an example of the `templateParameters`JSON object within the request
body to
[deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
for the ERC-1155 Multi-Token template.
In this example, the `defaultAdmin`, `primarySaleRecipient`, and
`royaltyRecipient` parameters are the same address but can be set distinctly
based on your use case.
```json JSON theme={null}
...
"templateParameters": {
"name": "My Multi-Token Contract",
"defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"royaltyRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"royaltyPercent": 0.05
}
```
## Common functions
This section lists the most commonly used functions on the Multi-Token template,
along with their respective parameters and potential failure scenarios. These
functions include:
* [mintTo \[write\]](#mintto-write)
* [safeTransferFrom \[write\]](#safetransferfrom-write)
* [setApprovalForAll \[write\]](#setapprovalforall-write)
* [setTokenURI \[write\]](#settokenuri-write)
* [burn \[write\]](#burn-write)
* [safeBatchTransferFrom \[write\]](#safebatchtransferfrom-write)
* [balanceOfBatch \[read\]](#balanceofbatch-read)
* [balanceOf \[read\]](#balanceof-read)
* [nextTokenIdToMint \[read\]](#nexttokenidtomint-read)
* [uri \[read\]](#uri-read)
At this time, not all failure scenarios or error messages received from the
blockchain are passed through Circle's APIs. Instead, you will receive a
generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error.
If available, the `errorDetails` field will have more information on the cause
of failure.
## mintTo \[write]
The `mintTo` function allows you to create new NFTs or increase the supply of
existing NFTs. It is a flexible function that can cater to both scenarios.
### Parameters
| Parameter | Type | Description |
| :--------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_to` | address | The address to which the newly minted NFT will be assigned. |
| `_tokenId` | unit256 | The unique identifier for the NFT. If the value is set to `type(uint256).max`, the function will assign the next available token ID. Otherwise, it will assign the provided `_tokenId` value. |
| `_uri` | calldata | The Uniform Resource Identifier (URI) for the NFT's metadata. It specifies the location from where the metadata can be retrieved. |
| `_amount` | unit256 | The amount of the newly minted NFTs to be assigned. |
### Failure scenarios
* **Insufficient Role:** The `mintTo` function is defined with the
`onlyRole(MINTER\_ROLE)` modifier, meaning only addresses with the
`MINTER\_ROLE` can call this function. The function will revert and fail if
the caller does not have the necessary role.
* **Token ID Overflow:** If the `_tokenId` parameter is set to
`type(uint256).max` (the maximum value for a uint256), the function will
attempt to create a new token and assign the next available token ID. However,
an overflow can occur if the `nextTokenIdToMint` variable has reached its
maximum value. This overflow condition will cause the function to fail.
* **Invalid Token ID:** If the `_tokenId` parameter is not set to
`type(uint256).max`, the function will attempt to mint an NFT with the
specified token ID. However, if the provided `_tokenId` value is greater than
or equal to the value of `nextTokenIdToMint`, the function will revert and
fail with the following error message.\
*"invalid id"*
* **Existing Token ID with Non-Empty URI:** When `_tokenId` is provided and
already exists, the function checks whether the associated metadata URI for
that token ID is empty. If the URI is not empty, the token has already been
minted and has an associated URI. In this case, the function will fail and
revert, preventing the same token ID from being minted multiple times.
* **Minting to Zero Address:** The function checks whether the `_to` address is
the zero address `address(0)`. Minting tokens to the zero address is
prohibited, as it represents an invalid or non-existent address. If `_to` is
the zero address, the function will fail and revert with the following error
message.\
*"ERC1155: mint to the zero address"*
* **Rejection by ERC1155Receiver Contract:** If the recipient address `_to` is a
contract, the function will attempt to call the `onERC1155Received` function
of that contract to check if the contract supports receiving the NFT. If the
contract's `onERC1155Received` function rejects the transfer by returning a
value other than `IERC1155ReceiverUpgradeable.onERC1155Received.selector`, the
function will revert and fail with the following error message.\
\_ "ERC1155: ERC1155Receiver rejected tokens"\_
### Notes
* **Creating New NFTs:** When you pass `type(uint256).max` via the `_tokenId`
parameter, the function will create a new NFT with `_tokenId` equal to
`nextTokenIdToMint` and assign it to the specified `_to` address. The `_uri`
parameter allows you to provide the metadata URI for the newly created NFT.
The `amount` parameter allows you to specify how many instances of this NFT
with the given ID should be minted.
* **Increasing Supply of Existing NFTs:** If you pass an existing token ID via
the `tokenId` parameter, the function will increase the supply of that
specific NFT. Instead of creating a new token ID, the function will mint
additional instances of the existing NFT, adding to the current supply. Again,
the `_amount` parameter determines how many additional instances of the NFT
should be minted.
```solidity Solidity theme={null}
// Lets an account with MINTER_ROLE mint an NFT.
function mintTo(
address _to,
uint256 _tokenId,
string calldata _uri,
uint256 _amount
) external onlyRole(MINTER_ROLE) {
uint256 tokenIdToMint;
if (_tokenId == type(uint256).max) {
tokenIdToMint = nextTokenIdToMint;
nextTokenIdToMint += 1;
} else {
require(_tokenId < nextTokenIdToMint, "invalid id");
tokenIdToMint = _tokenId;
}
// `_mintTo` is re-used. `mintTo` just adds a minter role check.
_mintTo(_to, _uri, tokenIdToMint, _amount);
}
```
## safeTransferFrom \[write]
The `safeTransferFrom` function allows for transferring a specified amount of a
particular token ID from one address `from` to another address `to`.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from` | address | The address of the current token owner, from whom the tokens will be transferred. |
| `to` | address | The address of the recipient who will receive the transferred tokens. |
| `id` | uint256 | The unique identifier for transferring the token. |
| `amount` | uint256 | The amount of tokens being transferred. This represents the number of tokens to be transferred. |
| `data` | bytes | Optional additional data to pass to the receiver contract if it is a contract. This can include custom arguments or instructions for the receiving contract. |
### Failure scenarios
* **Transfer to Zero Address:** The function verifies if the to address is the
zero address `address(0)`. Transfers to the zero address are not permitted, as
it represents an invalid or non-existent address. If the `to` address is the
zero address, the function fails and reverts with the following error
message.\
*"ERC1155: transfer to the zero address"*
* **Insufficient Balance:** The function checks if the from address has a
sufficient balance of the specified token ID (id) to perform the transfer. If
the balance exceeds the specified amount, the function fails and reverts to
the following error message.\
*"ERC1155: insufficient balance for transfer"*
* **Caller Not Authorized:** The function verifies if the caller of the
`_msgSender()` function is either the owner of the tokens (`from`) or has been
approved as an operator for `from`. If the caller is neither the token owner
nor an approved operator, the function fails and reverts with the following
error message.\
*"ERC1155: caller is not token owner or approved"*
* **Before/After Token Transfer Hooks:** The function calls the
`_beforeTokenTransfer` and `_afterTokenTransfer` hooks to update any necessary
state or perform additional checks. These hooks may contain custom business
logic that can cause the transfer to fail if certain conditions are not met.
* **ERC1155Receiver Contract Rejection:** If the `to` address is a contract, the
function attempts to call the `onERC1155Received` function of that contract to
check if the contract supports receiving the tokens. If the contract's
`onERC1155Received` function rejects the transfer by returning a value other
than `IERC1155ReceiverUpgradeable.onERC1155Received.selector`, the function
fails and reverts with the following error message.\
*"ERC1155: ERC1155Receiver rejected tokens"*
```solidity Solidity theme={null}
// See IERC1155-safeTransferFrom.
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual override {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()),
"ERC1155: caller is not token owner or approved"
);
_safeTransferFrom(from, to, id, amount, data);
}
```
## setApprovalForAll \[write]
The `setApprovalForAll` function is used to set the approval status for an
operator to manage all tokens of the caller (owner) on their behalf.
### Parameters
| Parameter | Type | Description |
| :--------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `operator` | address | The operator's address for whom the approval status is set. The operator will be able to manage all tokens owned by the caller. |
| `approved` | bool | The boolean value indicates whether the operator is approved (true) or disapproved (false) to manage all tokens on behalf of the caller. |
### Failure scenarios
* The function requires that the caller (owner) cannot set the approval status
for themselves. If the operator address provided is the same as the owner
address, the function will fail with the given following error message.\
*"ERC1155: setting approval status for self"*
### Notes
* Once an operator is approved using the `setApprovalForAll` function, they can
act on behalf of the token owner. This includes performing actions such as
transferring tokens.
```solidity Solidity theme={null}
// See {IERC1155-setApprovalForAll}.
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
// Approve `operator` to operate on all of `owner` tokens
// Emits an {ApprovalForAll} event.
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
require(owner != operator, "ERC1155: setting approval status for self");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
```
## setTokenURI \[write]
The `setTokenURI` function is used to set the metadata URI for a given NFT.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :--------------------------------------------------------------------------- |
| `tokenId` | unit256 | The unique identifier of the NFT for which the metadata URI needs to be set. |
| `uri` | string | The URI string represents the metadata's location associated with the NFT. |
### Failure scenarios
* The function requires the metadata URI to have a length greater than zero.
This error occurs when the input parameter `_uri` is an empty string.\
\_"NFTMetadata: empty metadata" \_
* This error occurs if the `_canSetMetadata()` function returns false. It
indicates that the caller has no authority or permission to set the metadata
for the given NFT.\
*"NFTMetadata: not authorized to set metadata"*
* This error occurs when `uriFrozen` is true, indicating that the metadata is
frozen and cannot be updated.\
*"NFTMetadata: metadata is frozen"*
```solidity Solidity theme={null}
// Sets the metadata URI for a given NFT.
function setTokenURI(uint256 _tokenId, string memory _uri) public virtual {
require(_canSetMetadata(), "NFTMetadata: not authorized to set metadata.");
require(!uriFrozen, "NFTMetadata: metadata is frozen.");
_setTokenURI(_tokenId, _uri);
}
// Sets the metadata URI for a given NFT.
function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual {
require(bytes(_uri).length > 0, "NFTMetadata: empty metadata.");
_tokenURI[_tokenId] = _uri;
emit MetadataUpdate(_tokenId);
}
```
## burn \[write]
This function allows a token owner to burn a specified amount (value) of tokens
they own.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------------- |
| `account` | address | The address of the token owner who wants to burn their tokens. |
| `id` | unit256 | The unique identifier of the token to be burned. |
| `value` | unit256 | The amount of tokens to be burned. |
### Failure scenarios
* This error occurs when the caller of the burn function is neither the owner of
the tokens nor approved to burn them. The caller must either be the account
that owns the tokens or have approval from the owner to burn the tokens.\
\_"ERC1155: caller is not owner nor approved" \_
* The function checks that the burning amount does not exceed the available
balance. This error occurs if the amount of tokens specified to be burned
(`amount`) is greater than the balance of tokens (`fromBalance`) owned by the
specified account.\
*"ERC1155: burn amount exceeds balance"*
* This error occurs if the from address (the address from which the tokens are
being burned) is the zero address (0x000...). This address is generally
reserved as an invalid or non-existent address and cannot be used for token
burning.\
*"ERC1155: burn from the zero address"*
```solidity Solidity theme={null}
// Lets a token owner burn the tokens they own (i.e. destroy for good)
function burn(address account, uint256 id, uint256 value) public virtual {
require(
account == _msgSender() || isApprovedForAll(account, _msgSender()),
"ERC1155: caller is not owner nor approved."
);
_burn(account, id, value);
}
```
## safeBatchTransferFrom \[write]
This function enables the safe transfer of multiple ERC1155 tokens from one
address (`from`) to another address (`to`) in a batch.
### Parameters
| Parameter | Type | Description |
| :-------- | :--------- | :------------------------------------------------------------------------------------- |
| `from` | address | The address from which the tokens are transferred. |
| `to` | address | The address to which the tokens are transferred. |
| `ids` | uint256\[] | An array of unique identifiers of the tokens to be transferred. |
| `amounts` | uint256\[] | An array specifying the corresponding amounts of tokens to be transferred for each ID. |
| `data` | bytes | Additional data to pass along with the transfer. Optional parameter. |
### Failure scenarios
* This error occurs if the caller of the function is neither the token owner nor
approved to perform the transfer. The caller must either be the from address
or have approval from the from address to transfer the tokens.\
*"ERC1155: caller is not token owner or approved"*
* This error occurs if the lengths of the `ids` and amounts arrays do not match.
Each ID should have a corresponding amount to be transferred. The arrays
should have the same length.\
*"ERC1155: ids and amounts length mismatch"*
* This error occurs if the `to` address is the zero address (0x000...).
Transferring tokens to the zero address is not allowed as it is generally used
to represent an invalid or non-existent address.\
*"ERC1155: transfer to the zero address"*
* This error occurs if the from address does not have a sufficient balance of
tokens to transfer. The function checks that the from address has enough
tokens of each ID to fulfill the transfer.\
*"ERC1155: insufficient balance for transfer"*
* This error occurs if the to address is a contract and the contract does not
implement the `onERC1155BatchReceived` function from the
`IERC1155ReceiverUpgradeable` interface or if the function returns a value
other than `onERC1155BatchReceived.selector`. This check ensures that the
receiving contract can handle the transferred tokens properly.\
*"ERC1155: ERC1155Receiver rejected tokens"*
```solidity Solidity theme={null}
// IERC1155-safeBatchTransferFrom
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()),
"ERC1155: caller is not token owner or approved"
);
_safeBatchTransferFrom(from, to, ids, amounts, data);
}
```
## balanceOfBatch \[read]
The `balanceOfBatch` function retrieves the balances of multiple accounts for
multiple token IDs in a single function call.
### Parameters
| Parameter | Type | Description |
| :--------- | :--------- | :------------------------------------------------------------------------- |
| `accounts` | address\[] | An array of addresses representing the accounts to query the balances for. |
| `ids` | unit256\[] | An array of unique identifiers of the tokens to query the balances for. |
### Failure scenarios
* **Mismatched Array Lengths:** The function requires that the length of the
accounts array is equal to the length of the `ids` array. If this condition is
not met, it will throw a required exception with the following error
message.\
*"ERC1155: accounts and ids length mismatch"*
```solidity Solidity theme={null}
// IERC1155-balanceOfBatch
// Requirements:
// `accounts` and `ids` must have the same length.
function balanceOfBatch(
address[] memory accounts,
uint256[] memory ids
) public view virtual override returns (uint256[] memory) {
require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}
```
## balanceOf \[read]
The `balanceOf` function retrieves the balance of a specific account for a
particular token ID.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------------------ |
| `account` | address | The EVM address for which the balance is being queried. |
| `id` | unit256 | The unique token identifier for which the balance is being queried. |
### Failure scenarios
* **Zero Address:** The function requires that the account parameter is not set
to the zero address `address(0)`. If this condition is not met, it will throw
a required exception with the following error message.\
*"ERC1155: address zero is not a valid owner".*
```solidity Solidity theme={null}
// See IERC1155-balanceOf
// Requirements:
// account cannot be the zero address.
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: address zero is not a valid owner");
return _balances[id][account];
}
```
## uri \[read]
The URI function retrieves the associated URI with a specific token ID. This URI
provides a way to access metadata and additional information about the token.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :---------------------------------------------------------- |
| `tokenId` | unit256 | This is the unique token identifier for retrieving the URI. |
```solidity Solidity theme={null}
// Returns the URI for a tokenId
function uri(uint256 _tokenId) public view override returns (string memory) {
return _tokenURI[_tokenId];
}
```
## Public Variables
Public variables are accessible from within the contract and can be accessed
from external contracts. Solidity automatically generates a getter function for
public state variables.
## nextTokenIdToMint \[read]
The `nextTokenIdToMint` variable is a public constant on the smart contract. An
unsigned integer (uint256) represents the next token ID minted or created when
`type(uint256).max` is passed to the `mintTo` function.
```solidity Solidity theme={null}
// The next token ID of the NFT to mint.
uint256 public nextTokenIdToMint;
```
# Token Template
Source: https://developers.circle.com/contracts/erc-20-token
The Token template is an audited, ready-to-deploy smart contract for an ERC-20
token. The ERC-20 standard is the most popular standard for fungible tokens. The
token's fungibility allows it to be used for a variety of use cases, including:
* **Stablecoins:** ERC-20 tokens serve as the foundation for stablecoins like
USDC, backed by US dollars. To learn more about the USDC contract, see
[0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)
on Etherscan.
* **Loyalty points:** ERC-20 tokens can represent on-chain loyalty points to
incentivize and reward users for their activities within a platform or
ecosystem.
* **Governance:** ERC-20 tokens can represent governance rights, allowing
holders to participate in protocol decisions. Notable examples include tokens
utilized by platforms like Uniswap.
* **Ownership:** ERC-20 tokens can also represent fractional ownership in
real-world assets, such as houses, ounces of gold, or company shares.
In this comprehensive guide, you explore the Token template, which provides all
the necessary information to deploy and understand the contract's common
functions.
## Deployment parameters
The Token template creates a customized, fully compliant ERC-20 smart contract.
To create a contract using this template, provide the following parameter values
when deploying a smart contract template using the
[`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
API.
**Template ID:** a1b74add-23e0-4712-88d1-6b3009e85a86
### Template deployment parameters
| Parameter | Type | Required | Description |
| ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | String | X | Name of the contract - stored as a property of the contract on-chain. |
| `symbol` | String | | Symbol of the token - stored on-chain. The symbol is usually 3 or 4 characters in length. |
| `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract. **Important:** You will lose administrative access to the contract if this is not set to an address you control. |
| `primarySaleRecipient` | String | X | The recipient address for first-time sales. |
| `platformFeeRecipient` | String | | The recipient address for all sales fees. If you deploy a template on someone else's behalf, you can set this to your own address. |
| `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to the `platformFeeRecipient`. |
| `contractUri` | String | | The URL for the marketplace metadata of your contract. This is used on marketplaces like OpenSea. See [Contract-level Metadata](https://docs.opensea.io/docs/contract-level-metadata) for more information. |
| `trustedForwarders` | Strings\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. See [ethereum.org](https://eips.ethereum.org/EIPS/eip-2771) for more information. |
Here is an example of the `templateParameters` JSON object within the request
body to
[deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
for the ERC-20 Token template.
In this example, the `defaultAdmin` and `primarySaleRecipient` parameters are
the same address but can be set distinctly based on your use case.
```json JSON theme={null}
"templateParameters": {
"name": "My Token Contract",
"defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6"
}
```
## Common functions
This section lists the most commonly used functions on the Token template, their
respective parameters and potential failure scenarios. These functions include:
* [approve \[write\]](#approve-write)
* [transfer \[write\]](#transfer-write)
* [mintTo \[write\]](#mintto-write)
* [burn \[write\]](#burn-write)
* [grantRole \[write\]](#grantrole-write)
* [revokeRole \[write\]](#revokerole-write)
* [balanceOf \[read\]](#balanceof-write)
* [allowance \[read\]](#allowance-write)
At this time, not all failure scenarios or error messages received from the
blockchain are passed through Circle's APIs. Instead, you will receive a
generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error.
If available, the `errorDetails` field will have more information on the cause
of failure.
## approve \[write]
The `approve` function lets token owners specify a limit on the number of tokens
another account address can spend on their behalf.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spender` | address | The owner approves the account address to spend tokens. It could be another smart contract address or an externally owned account address. |
| `amount` | unit256 | The number of tokens the owner approves for spending by the specified spender. The amount should be set in the smallest denomination of the ERC-20 token. |
### Failure scenarios
* The approve function fails if the `spender` is the zero address.\
*"ERC20: approve to the zero address"*
### Notes
* If `amount` is the maximum uint256 value the allowance is not updated when the
`transferFrom` function is called. This is semantically equivalent to an
infinite approval.
* There is no balance check on the `approve` function so token owners may
approve allowances greater than their current balance.
```solidity Solidity theme={null}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
```
## transfer \[write]
Allows the token owner to transfer a specified number of tokens to another
account address.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------------------------------------------------------------------ |
| `to` | address | The address to which the token will be transferred. This can be another user's address or a smart contract address. |
| `amount` | unit256 | The number of tokens to transfer. |
### Failure scenarios
* The `to` address parameter is checked to ensure it is not the zero address
`address(0)`.\
*"ERC20: transfer to the zero address"*
* The `beforeTokenTransfer` hook is called, which checks `TOKEN_TRANSFER`
permissions.\
"restricted to TRANSFER\_ROLE holders”
* The token owner doesn't have a sufficient balance to transfer the specified
token amount.\
*"ERC20: transfer amount exceeds balance"*
```solidity Solidity theme={null}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
```
## mintTo \[write]
A designated minter can create a specified number of tokens and assign them to a
specified account address.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :----------------------------------------------------------------------- |
| `to` | address | The address to which the newly minted tokens will be assigned. |
| `amount` | unit256 | The number of tokens to be minted and assigned to the specified address. |
### Failure scenarios
* The function checks if the caller has the `MINTER_ROLE`. The function will
fail if the caller does not have the minter role. Expect to see the following
error:\
*"not minter."*
* The function ensures that the account address is not the zero address
`address(0)`, as this is an invalid address to mint tokens to.\
*"ERC20: mint to the zero address"*
* The `beforeTokenTransfer` hook is called, which checks `TOKEN_TRANSFER`
permissions.\
*"restricted to* TRANSFER*ROLE \_holders”*
```solidity Solidity theme={null}
function mintTo(address to, uint256 amount) public virtual {
require(hasRole(MINTER_ROLE, _msgSender()), "not minter.");
_mintTo(to, amount);
}
```
## burn \[write]
Allows a token holder to burn (destroy) a specified number of the token total
supply.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :--------------------------------- |
| `amount` | unit256 | The number of tokens to be burned. |
### Failure scenarios
* The `_beforeTokenTransfer` hook is called, which checks `TOKEN_TRANSFER`
permissions.\
*"restricted to TRANSFER\_ROLE holders”*
* The token owner has no sufficient balance to burn the specified token
amount.\
*"ERC20: burn amount exceeds balance"*
```solidity Solidity theme={null}
function burn(uint256 amount) public virtual {
_burn(_msgSender(), amount);
}
```
### Permissions and roles
Roles are referred to by their `bytes32` identifier. For example:
```solidity Solidity theme={null}
bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
```
Roles can also represent a set of permissions. To restrict access to a function
call, use `hasRole`:
```solidity Solidity theme={null}
function foo() public {
require(hasRole(MY_ROLE, msg.sender));
...
}
```
Roles can be granted and revoked dynamically via the `grantRole` and
`revokeRole` functions. Each role has an associated admin role, and only
accounts that have a role's admin role can call `grantRole` and `revokeRole`. By
default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`. Only accounts
with the `DEFAULT_ADMIN_ROLE` can grant or revoke other roles. You can use the
`_setRoleAdmin` function to create more complex role relationships.
## grantRole \[write]
Grants a specified role to an account. Only an account address that has the
admin role assigned can call this function.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------ |
| `role` | bytes32 | The bytes32 identifier of the role to be granted. |
| `account` | address | The address to which the role will be granted. |
### Failure scenarios
* The `onlyRole(getRoleAdmin(role))` modifier ensures that the function caller
has the admin role for the specified role. Only addresses with the admin role
can grant roles to other accounts.\
*"AccessControl: account ", StringsUpgradeable.toHexString(account), " is
missing role ", StringsUpgradeable.toHexString(uint256(role), 32)*
* The function checks if the account already has the specified role using the
`hasRole` function. If the account does not have the role, the function
continues. There is no error message.
```solidity Solidity theme={null}
function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
```
## revokeRole \[write]
Revoke the specified role from an account address. Only account addresses with
the admin role assigned can call this function.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :----------------------------------------------- |
| `role` | bytes32 | The role to be revoked. |
| `account` | address | The address from which the role will be revoked. |
### Failure scenarios
* The `onlyRole(getRoleAdmin(role))` modifier ensures that the function caller
has the admin role for the specified role. Only addresses with the admin role
can revoke roles from other accounts.\
*"AccessControl: account ", StringsUpgradeable.toHexString(account), " is
missing role ", StringsUpgradeable.toHexString(uint256(role), 32)*
```solidity Solidity theme={null}
function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
```
## balanceOf \[read]
Retrieves the balance of tokens owned by a specific account address.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :-------------------------------------------------------- |
| `account` | address | The address for which the token balance is being fetched. |
```solidity Solidity theme={null}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
```
## allowance \[read]
Returns the maximum amount the spender is approved to withdraw from the owner's
account. This function retrieves the allowance granted by the owner account
address to the spender account address.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :---------------------------------------------------- |
| `owner` | address | The address that granted the allowance. |
| `spender` | address | The address for which the allowance is being fetched. |
```solidity Solidity theme={null}
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
```
# NFT Template
Source: https://developers.circle.com/contracts/erc-721-nft
The NFT template is an audited, ready-to-deploy smart contract for creating and
managing NFTs. It implements the ERC-721 standard, which is widely used for
representing non-fungible tokens (NFTs) on a blockchain. Unlike ERC-20 tokens,
which represent fungible and interchangeable assets, ERC-721 NFTs are unique and
non-interchangeable, making them suitable for digital collectibles, gaming
assets, and many other use cases.
The ERC-721 NFT standard has gained significant popularity and has been
implemented by numerous projects and platforms. Some key use cases for ERC-721
NFTs include:
* **Digital collectibles:** ERC-721 NFTs are extensively used for creating and
trading unique digital collectibles. These collectibles can represent various
items such as artwork, trading cards, virtual pets, in-game assets, and more.
* **Tokenized assets:** ERC-721 NFTs can represent ownership in real-world
assets such as real estate, artwork, jewelry, and other physical assets. This
enables fractional ownership, providing liquidity and opening up investment
opportunities.
* **Gaming assets:** ERC-721 NFTs are a perfect fit for representing in-game
assets, enabling players to own, trade, and transfer virtual items securely
and transparently. This functionality has facilitated the emergence of
blockchain-based gaming ecosystems.
In this comprehensive guide, you explore the NFT template, which provides all
the necessary information to deploy and understand the contract's common
functions.
## Deployment parameters
The NFT template creates a customized, fully compliant ERC-721 smart contract.
To create a contract using this template, provide the following parameter values
when deploying a smart contract template using the
[`POST: /templates/{id}/deploy`](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
API.
**Template ID:** 76b83278-50e2-4006-8b63-5b1a2a814533
### Template deployment parameters
| Parameter | Type | Required | Description |
| :--------------------: | --------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| `name` | String | X | Name of the contract - stored as a property of the contract on-chain. |
| `symbol` | String | | Symbol of the token - stored onchain. The symbol is usually 3 or 4 characters in length. |
| `defaultAdmin` | String | X | The address of the default admin. This address can execute permissioned functions on the contract. You will lose administrative access to the contract if this is not set to an address you control. |
| `primarySaleRecipient` | String | X | The recipient address for first-time sales. |
| `platformFeeRecipient` | String | | The recipient address for all sale fees. You can set this to your address if you are deploying a template on someone else's behalf. |
| `platformFeePercent` | Float | | The percentage of sales that go to the platform fee recipient. For example, set it as 0.1 if you want 10% of sales fees to go to *platformFeeRecipient*. |
| `royaltyRecipient` | String | X | The recipient address for all royalties (secondary sales). This allows the contract creator to benefit from further sales of the contract token. |
| `royaltyPercent` | Float | X | The percentage of secondary sales that go to the royalty recipient. For example, set it as 0.05 if you want royalties to be 5% of secondary sales value. |
| `contractUri` | String | | The URL for the marketplace metadata of your contract. This is used on marketplaces like OpenSea. See [Contract-level Metadata](https://docs.opensea.io/docs/contract-level-metadata) for more information. |
| `trustedForwarders` | String\[] | | A list of addresses that can forward ERC2771 meta-transactions to this contract. See [ethereum.org](https://eips.ethereum.org/EIPS/eip-2771) for more information. |
Here is an example of the `templateParameters` JSON object within the request
body to
[deploy a contract from a template](/api-reference/contracts/smart-contract-platform/deploy-contract-template)
for the ERC-721 NFT template.
In this example, the `defaultAdmin`, `primarySaleRecipient`, and
`royaltyRecipient` parameters are the same address but can be set distinctly
based on your use case.
```json JSON theme={null}
...
"templateParameters": {
"name": "My NFT Contract",
"defaultAdmin": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"primarySaleRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"royaltyRecipient": "0x4F77E56dfA40990349e1078e97AC3Eb479e0dAc6",
"royaltyPercent": 0.05
}
```
## Common functions
This section lists the most commonly used functions on NFT template, their
respective parameters and potential failure scenarios. These functions include:
* [approve \[write\]](#approve-write)
* [mintTo \[write\]](#mintto-write)
* [safeTransferFrom \[write\]](#safetransferfrom-write)
* [setTokenURI \[write\]](#settokenuri-write)
* [ownerOf \[read\]](#ownerof-read)
* [balanceOf \[read\]](#balanceof-address-owner-read)
At this time, not all failure scenarios or error messages received from the
blockchain are passed through Circle's APIs. Instead, you will receive a
generic [`ESTIMATION_ERROR`](/contracts/error-codes#transaction-errors) error.
If available, the `errorDetails` field will have more information on the cause
of failure.
## approve \[write]
The approve function allows the owner of an ERC721 NFT to approve another
address to transfer the token on their behalf.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------- |
| `to` | address | The address approved to transfer the token. |
| `tokenId` | unit256 | The identifier of the token being approved for transfer. |
**Failure Scenarios:**
* If the *to* address matches the current owner of the token (owner), the
function will fail. This check ensures that the approval is not granted to the
same owner, preventing unnecessary approvals. *"ERC721: approval to current
owner"*
* The function requires that the caller `_msgSender` either be the token's owner
or have been approved for all by the owner. If this condition is not met, the
function will fail. This validation prevents unauthorized users from approving
transfers on behalf of the token owner. *"ERC721: approve caller is not token
owner or approved for all"*
```solidity Solidity theme={null}
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721Upgradeable.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
```
## mintTo \[write]
The `mintTo` function is a function that mints a new NFT and assigns it to a
specific address. This function can only be called by an address with the
`MINTER_ROLE`.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------------- |
| `to` | address | The address to which the minted NFT will be assigned. |
| `uri` | string | The URI (Uniform Resource Identifier) of the newly minted NFT. |
### Returns
| Parameter | | |
| :-------------- | :------ | :--------------------------------------- |
| `tokenIdToMint` | uint256 | The unique identifier of the minted NFT. |
### Failure scenarios
* If the caller of the function does not have the `MINTER_ROLE` assigned, the
function will fail and throw an exception. *"AccessControl: account ",
StringsUpgradeable.toHexString(account), " is missing role ",
StringsUpgradeable.toHexString(uint256(role), 32)*
* The function checks if the length of the \_uri string is greater than 0,
ensuring that the URI is not empty.\
*"empty uri."*
* *The function checks that the to address is not the zero address.*\
*"ERC721: mint to the zero address"*
* The function checks that the `tokenId` has not already been created. *"ERC721:
token already minted"*
```solidity Solidity theme={null}
function mintTo(address _to, string calldata _uri) external onlyRole(MINTER_ROLE) returns (uint256) {
// `_mintTo` is re-used. `mintTo` just adds a minter role check.
return _mintTo(_to, _uri);
}
function _mintTo(address _to, string calldata _uri) internal returns (uint256 tokenIdToMint) {
tokenIdToMint = nextTokenIdToMint;
nextTokenIdToMint += 1;
require(bytes(_uri).length > 0, "empty uri.");
_setTokenURI(tokenIdToMint, _uri);
_safeMint(_to, tokenIdToMint);
emit TokensMinted(_to, tokenIdToMint, _uri);
}
```
## safeTransferFrom \[write]
This function allows the transfer of an ERC721 NFT from the `from` address to
the `to` address. It requires that the caller is the token owner or has been
approved to transfer the token.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :-------------------------------------------------------- |
| `from` | address | The address that owns the token and wants to transfer it. |
| `to` | address | The address that will receive ownership of the token. |
| `tokenId` | unit256 | The unique identifier of the token being transferred. |
### Failure scenarios
* The `isApprovedOrOwner` function is called to check if the caller is the token
owner or an approved address. This check ensures that the transfer can only be
performed by the token owner or an approved address.\
*"ERC721: caller is not token owner or approved"*
* If the to address is a contract, the `checkOnERC721Received` function is
called to check if the to address is a contract that implements the
`onERC721Received` function correctly - according to the ERC721 standard.
* It checks if the token being transferred `tokenId` parameter is owned by the
`from` address parameter. *"ERC721: transfer from incorrect owner"*
* It checks that the `to` address parameter is not the zero address. If it is
the zero address, the function throws an exception with the message.\
*"ERC721: transfer to the zero address"*
* If the transfer is restricted on the contract, it still allows burning and
minting. It checks whether the `TRANSFER_ROLE` is assigned to either the
`from` or `to` address. This ensures that token transfers comply with specific
access control restrictions defined by the contract.\
*"restricted to TRANSFER\_ROLE holders”*
* The function will check if the `to` address is a contract. If it is, the
`_checkOnERC721Received` hook will check if the receiver address properly
handles the received token. I\
*"ERC721: transfer to non ERC721Receiver implementer"*
```solidity Solidity theme={null}
// safeTransferFrom
function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
// safeTransferFrom - with data parameter
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
```
## setTokenURI \[write]
This function is responsible for setting the metadata URI for a specific NFT
token.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :------------------------------------------------------------------------------ |
| `tokenId` | unit256 | The unique identifier of the NFT token for which the metadata URI is being set. |
| `uri` | string | The new metadata URI that will be associated with the NFT token. |
### Failure scenarios
* The function checks if the caller is authorized to set the metadata URI. It
calls the `canSetMetadata` function, which checks the authorization based on
certain conditions specified in the contract.\
*"NFTMetadata: not authorized to set metadata."*
* The function verifies if the metadata URI is not frozen. It checks the
`uriFrozen` boolean flag to determine if the metadata is in a frozen state. If
the metadata is frozen, meaning it cannot be changed, the function will throw
an exception.\
*"NFTMetadata: metadata is frozen."*
* If the provided URI is empty, the function will throw an exception with the
message\
*"NFTMetadata: empty metadata."*
```solidity Solidity theme={null}
function setTokenURI(uint256 _tokenId, string memory _uri) public virtual {
require(_canSetMetadata(), "NFTMetadata: not authorized to set metadata.");
require(!uriFrozen, "NFTMetadata: metadata is frozen.");
_setTokenURI(_tokenId, _uri);
}
function _setTokenURI(uint256 _tokenId, string memory _uri) internal virtual {
require(bytes(_uri).length > 0, "NFTMetadata: empty metadata.");
_tokenURI[_tokenId] = _uri;
emit MetadataUpdate(_tokenId);
}
```
## ownerOf \[read]
This function is used to retrieve the address of the owner of the ERC721 NFT
with the specified `tokenId`.
### Parameters
| Parameter | Type | Description |
| :-------- | :------ | :--------------------------------------------------------------------------------- |
| `tokenId` | unit256 | The unique identifier of the token for which the owner's address is being fetched. |
### Failure scenarios
* The function checks if the owner's address is not the zero address. This check
is performed to ensure that a valid owner address is returned. *"ERC721:
invalid token ID"*
### Note
* The function does not revert if the token doesn't exist. The zero address will
be returned.
```solidity Solidity theme={null}
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
```
## balanceOf \[read]
This function retrieves the balance (number of tokens) owned by a specific owner
address.
### Parameters
| Parameter | | |
| :-------- | :------ | :-------------------------------------------------------- |
| `owner` | address | The address for which the token balance is being fetched. |
### Failure scenarios
* The function checks if the `owner` address is not the zero address. The zero
address represents an invalid or nonexistent address. *"ERC721: address zero
is not a valid owner"*
```solidity Solidity theme={null}
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
```
# Contracts API Error Codes
Source: https://developers.circle.com/contracts/error-codes
Descriptions of error codes returned by the Contracts API
The Wallets and Contracts APIs return an
[HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) when
they encounter an error in an API request:
* `4xx` errors are client errors, which are informative and actionable. They
communicate a mistake to the user and suggest fixes.
* `5xx` errors are unexpected server-side errors.
The tables in the following sections describe some of the common error messages
you might encounter using the Wallets and Contracts APIs. Where possible, a
suggested fix is provided in the `Description` column.
## General error format
HTTP status codes do not always provide sufficient information to determine the
cause of the error. Since the status code is part of the header of the response,
the body of response contains additional, Circle-specific, error information.
For example, if a request contains an invalid parameter, the response includes
the following:
**Header**
`HTTP/1.1 400 Bad RequestContent-Type: application/json`
**Body**
```json JSON theme={null}
{
"code": 2,
"message": "API parameter invalid"
}
```
## Extended error format
In some cases, the response includes extended information about the cause of the
error. For example, if a request doesn't include a value for a required
parameter, the response includes a detailed error message:
**Header**
`HTTP/1.1 400 Bad RequestContent-Type: application/json`
**Body**
```json JSON theme={null}
{
"code": 2,
"message": "API parameter invalid",
"errors": [
{
"error": "required",
"message": "fail to bind request to CreateWalletSetRequest: EOF",
"location": "field1",
"invalidValue": "null",
"constraints": {}
}
]
}
```
## General errors
This section describes some common extended error messages you might encounter
using Wallets and Contracts APIs.
The following errors are general errors that can be returned for any request.
| Error Code | HTTP code | Error Message | Description |
| :--------- | :-------- | :--------------------- | :--------------------------------------------------------------- |
| `-1` | `400` | `Something went wrong` | An unknown error occurred while processing the API request. |
| `3` | `403` | `Forbidden` | The API key used does not have access to the requested endpoint. |
### Invalid requests
A `400` HTTP status code indicates that the request is invalid or malformed.
When the request is invalid, the Circle-specific error code is `2`. The
following table describes the common error messages that can be returned for
invalid requests.
| Error Code | HTTP code | Error Message | Description |
| :--------- | :-------- | :------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------- |
| `2` | `400` | `Invalid Entity` | There is an error in the JSON format passed in the request. |
| `2` | `400` | `Fail to bind request to parameter: invalid UUID format` | The parameter must be in UUID format. |
| `2` | `400` | `Error: Field validation` | One of the fields in the request is invalid. |
| `2` | `400` | `Error: Field validation for blockchain failed on the blockchain tag` | The blockchain does not exist. Try again with a [supported blockchain](/wallets/supported-blockchains). |
| `2` | `400` | `Error: Field validation for gasLimit failed on the required_without tag` | If `feeLevel` is provided, `gasLimit` should be set to `NULL`. If `feeLevel` is `NULL`, `gasLimit` must be provided. |
| `2` | `400` | `Error: Field validation for parameter failed on the min tag` | The parameter is not in the correct format. |
| `2` | `400` | `Cannot unmarshal` | The JSON body of the request is not valid. |
| `2` | `400` | `INVALID: parameter empty` | The parameter is required for this request. Try the request again with this parameter. |
## Contracts errors
This section includes the errors that can be returned from the Circle Contracts
API requests.
### Contract errors
| Error code | HTTP code | Error message | Description |
| ---------- | --------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `175001` | `404` | `Contract not found.` | The specified contract does not exist in the system. |
| `175002` | `400` | `No ABI JSON for the target contract.` | Cannot execute a read function on a contract without the ABI JSON. |
| `175003` | `400` | `Constructor parameters length must match constructor signature.` | The number of constructor parameters must match the constructor signature. |
| `175004` | `409` | `Contract already exists.` | The contract already exists in the system. |
| `175005` | `400` | `Address is not a contract address.` | The given address is not associated with a smart contract. |
| `175006` | `400` | `Contract is archived.` | Attempted to interact with an archived contract. |
| `175007` | `400` | `Invalid ABI JSON.` | The inputted ABI JSON is not correctly formatted. |
| `175008` | `400` | `Multi-layered proxies are not supported.` | Importing a multi-layered proxy contract is not supported. |
| `175009` | `400` | `Contract deployment pending.` | Contract deployment must be completed before function execution is available. |
| `175010` | `400` | `ABI function not found.` | The ABI function was not found on the contract. |
| `175011` | `400` | `Empty update on a contract.` | An empty update for a contract is not allowed |
| `175012` | `400` | `Unable to query contract.` | Unable to query contract. Check your parameters and try again. |
| `175013` | `400` | `ABI function is not supported.` | The ABI function of the contract is not supported. |
### Template errors
| Error code | HTTP code | Error message | Description |
| ---------- | --------- | -------------------------------------------------- | ------------------------------------------------------------------- |
| `175201` | `404` | `Template not found.` | The specified template does not exist in the system. |
| `175202` | `400` | `Deploying this template is temporarily disabled.` | Deploying this template is temporarily disabled. |
| `175203` | `400` | `Invalid template deployment parameter.` | The request contains an invalid field in the template parameters. |
| `175204` | `400` | `Missing required template deployment parameter.` | The request is missing a required field in the template parameters. |
| `175205` | `400` | `Estimation is not supported.` | Estimating the deployment of this template is not supported. |
### Event subscription errors
| Error code | HTTP code | Error message | Description |
| ---------- | --------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
| `175301` | `404` | `Event subscription not found.` | The specified event subscription does not exist or is not accessible to the caller. |
| `175302` | `409` | `Event subscription already exist.` | The specified event has already been created for this contract. |
| `175303` | `400` | `The specified event signature does not exist.` | The specified event signature does not exist on this contract. |
### Common errors
| Error code | HTTP code | Error message | Description |
| ---------- | --------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `175401` | `400` | `Fail to parse id as UUID in url.` | The specified ID is invalid (must be in UUID format). Try again with a valid ID. |
| `175402` | `400` | `The specified blockchain is either not supported or deprecated.` | The specified blockchain is either not supported or deprecated. |
| `175403` | `409` | `Please use a new idempotency key.` | Use a new idempotency key and try again. |
| `175404` | `400` | `TEST_API key cannot be used with blockchain mainnets, or LIVE_API key cannot be used with blockchain testnets.` | TEST\_API key cannot be used with blockchain mainnets, or LIVE\_API key cannot be used with blockchain testnets. |
| `175405` | `401` | `TEST_API key or LIVE_API key is not found for the request.` | TEST\_API key or LIVE\_API key is not found for the request. |
| `175406` | `400` | `This feature is temporarily disabled.` | This feature is temporarily disabled. |
| `175407` | `400` | `The specified blockchain is unavailable.` | The specified blockchain is unavailable. Check the [Circle Status page](https://status.circle.com/) for more details. |
| `175408` | `404` | `Cannot find corresponding pagination cursor in the system.` | Cannot find corresponding pagination cursor in the system. |
| `175409` | `403` | `Entities with restrictions cannot perform this operation.` | Entities with restrictions cannot perform this operation. |
| `175410` | `400` | `invalid address format` | The address format is invalid. |
### Transaction errors
| Error code | HTTP code | Error message | Description |
| ---------- | --------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `177001` | `400` | `transaction nonce is inconsistent with sender's latest nonce` | The transaction nonce is inconsistent with the sender's latest nonce. |
| `177002` | `400` | `user op nonce can not be larger than 0 when smart contract wallet hasn't been deployed` | User op nonce can not be larger than 0 when the smart contract wallet hasn't been deployed. |
| `177003` | `400` | `failed to execute this request on EVM due to insufficient token when estimating fee` | Failed to execute this request on EVM due to insufficient tokens when estimating the fee. |
| `177004` | `400` | `the total cost of executing transaction is higher than the balance of the user's account when estimating fee` | When estimating the fee, the total cost of executing the transaction is higher than the balance of the user's account. |
| `177005` | `400` | `the sender address is not token owner or approved when estimating token transfer` | The sender address is not token owner or approved when estimating token transfer. |
| `177006` | `400` | `gas required exceeds allowance when estimating fee` | Gas required exceeds allowance when estimating fee. |
| `177007` | `400` | `estimate fee execution reverted` | Estimate fee execution reverted. |
| `177008` | `400` | `ABI function signature can't pack ABI parameter` | ABI function signature can't pack ABI parameter. |
| `177009` | `400` | `fails to perform transaction estimation` | Fails to perform transaction estimation. |
| `177010` | `400` | `maxFee * gasLimit exceed configurable max transaction fee (default is 1 native token)` | The `MaxFee` \* `GasLimit` exceeds the configurable max transaction fee (default is 1 native token). |
| `177011` | `400` | `transaction needs feeLevel or gasLimit provided` | The transaction requires `feeLevel` or `gasLimit` to be provided in the request. |
| `177012` | `400` | `sca transaction needs feeLevel provided` | The SCA transaction requires `feeLevel` to be provided in the request. |
| `177013` | `400` | `EIP1559 chains need maxFee/priorityFee provided` | EIP1559 chains require `maxFee` and `priorityFee` to be provided in the request. |
| `177014` | `400` | `priorityFee cannot be larger than maxFee in creating transaction request` | Creating transaction requests`priorityFee` cannot be larger than `maxFee`. |
| `177015` | `400` | `missing bytecode for contract deployment` | Missing bytecode for contract deployment. |
| `177016` | `400` | `cannot provide both WalletID and SourceAddress/Blockchain` | You cannot provide `WalletID` and `SourceAddress`/`Blockchain` in a request. |
| `177017` | `400` | `Invalid amount in contract execution request` | The `amount` in the contract execution request is invalid. |
| `177018` | `400` | `policy is not activated and cannot be used` | The Gas Station paymaster policy is not activated and cannot be used. |
| `177019` | `400` | `exceeded max daily transaction of the policy` | The Gas Station paymaster policy maximum daily transaction limit has been reached. |
| `177020` | `400` | `exceeded max spend USD per transaction of the policy` | The transaction cost exceeds the Gas Station paymaster policy maximum spend per transaction in USD. |
| `177021` | `400` | `exceeded max spend USD daily of the policy` | The Gas Station paymaster policy for maximum spending daily in USD has been reached. |
| `177022` | `400` | `exceeded max native token daily of the policy` | The Gas Station paymaster policy for maximum native tokens daily of the policy. |
| `177023` | `400` | `sender is in policy blocklist` | The sender is on the Gas Station paymaster policy blocklist. |
| `177024` | `400` | `wallet and request's blockchain mismatch.` | The wallet and blockchain in the request should be the same. |
| `177301` | `400` | `wallet is Frozen` | Frozen wallets can not be updated or interacted with; they can only be queried. |
| `177302` | `400` | `invalid sca wallet config` | The SCA wallet configuration is invalid. |
| `177303` | `400` | `sca wallet first-time transaction is still in progress` | The SCA wallet needs to wait for the first-time transaction to finish deploying the wallet before processing more transactions. |
| `177304` | `400` | `SCA account is not supported on the given blockchain` | The SCA account is not supported on the given blockchain. |
| `177305` | `400` | `Entity is not eligible for SCA account creation. Please check paymaster policy setup` | The entity is not eligible for SCA account creation. Check the Gas Station paymaster policy setup. |
| `177601` | `400` | `could be caused by either no such wallet or wallet is not accessible to the caller` | The target wallet cannot be found in the system. Either the specified wallet doesn't exist, or it's inaccessible to the caller. |
| `177602` | `400` | `reusing an entity secret ciphertext is not allowed. Please re-encrypt the entity secret to generate new ciphertext` | Reusing an entity's secret ciphertext is not allowed. Re-encrypt the entity secret to generate a new ciphertext. |
| `177603` | `400` | `entity is likely not properly set up during the onboarding process` | The corresponding entity cannot be found in the system. |
| `177604` | `400` | `the provided entity secret is invalid` | The provided entity secret is invalid. |
| `177605` | `400` | `the entity secret has not been set yet. Please provide encrypted ciphertext in the console` | The entity secret has not been set up on your account. Provide encrypted ciphertext in the console. |
| `177606` | `400` | `current entity secret is invalid. Please rotate the entity secret first` | The provided entity secret is invalid. Rotate the entity secret first and send another API request. |
| `177607` | `400` | `please use a new idempotency key` | Use a new idempotency key. |
| `177901` | `400` | `smart contract query failed` | Error when querying contract. Check parameters and try again. |
# Postman Collection
Source: https://developers.circle.com/contracts/postman
Use Circle's Postman collection to send API requests and explore the Smart Contract Platform APIs.
Circle's Postman collection provides template requests to help you learn about
the Smart Contract Platform APIs. These requests run on
[Postman](https://www.postman.com/), an API platform for learning, building, and
using APIs. The Postman Contracts workspace includes a collection that matches
the organization of the
[API reference](/api-reference/contracts/smart-contract-platform/list-contracts).
## Run in Postman
To use the Postman collection, select **Run in Postman** below. You can fork the
collection to your workspace, view the collection in the public workspace, or
import the collection into Postman.
* **Fork**: Creates a copy of the collection while maintaining a link to the
parent.
* **View**: Allows you to try out the API without importing anything into your
Postman suite.
* **Import**: Creates a copy of the collection but does not maintain a link to
Circle's copy.
| Collection | Link |
| :--------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contracts | [](https://app.getpostman.com/run-collection/21445022-20c27ad9-62c1-4c95-8adc-e2d2a4a473cd?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D21445022-20c27ad9-62c1-4c95-8adc-e2d2a4a473cd%26entityType%3Dcollection%26workspaceId%3D73acd722-fab9-49b0-9382-086659476258) |
## Authorization
Paste your [API key](/contracts/create-api-key) in the Authorization tab of the
collection. To store your API key as a variable for more advanced testing, see
Postman's
[using variables](https://learning.postman.com/docs/sending-requests/variables/)
guide.
# Quickstart: Deploy a Smart Contract using Bytecode
Source: https://developers.circle.com/contracts/scp-deploy-smart-contract
Deploy smart contract bytecode using Circle Contracts
This quickstart walks you through deploying a smart contract using the compiled
bytecode and ABI using Circle Contracts.
Circle Contracts provides an API for deploying, exploring, and interacting with
smart contracts. The platform offers a powerful toolset for developers to build
decentralized applications and for businesses to transition onchain.
This guide can also be followed to deploy smart contracts on the other
[supported blockchains](/contracts/supported-blockchains) by changing the
`blockchain` parameter in your request. Additionally, you can deploy to Mainnet
by swapping out the Testnet API key for a Mainnet API key. See the
[Testnet vs Mainnet](/circle-mint/references/sandbox-and-testing#transition-to-production)
guide for more details.
## **Prerequisites**
Before you begin, ensure you've:
1. Created [an API key in the Circle Console](/contracts/create-api-key).
2. [Registered your Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret).
## **Step 1: Project setup**
Set up your local development environment and install the required dependencies.
### 1.1 Set up a new project
Create a new directory, navigate to it and initialize a new project with de1ault
settings
```shell NodeJS theme={null}
mkdir scp-bytecode-deploy
cd scp-bytecode-deploy
npm init -y
npm pkg set type=module
```
```shell Python theme={null}
mkdir scp-bytecode-deploy
cd scp-bytecode-deploy
python3 -m venv .venv
source .venv/bin/activate
```
### 1.2 Install dependencies
In the project directory, install the required dependencies. This guide uses
SDKs for Circle
[developer-controlled wallets](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet)
and [Contracts](https://developers.circle.com/contracts).
```ts NodeJS theme={null}
npm install @circle-fin/developer-controlled-wallets @circle-fin/smart-contract-platform
```
```py Python theme={null}
pip install circle-smart-contract-platform circle-developer-controlled-wallets
```
## Step 2: Create a wallet and fund it with testnet tokens
In this section, you will create a developer-controlled wallet with the SDK and
fund it with testnet USDC to pay for the gas fees needed to deploy the smart
contract. If you already have a developer-controlled wallet, skip to
[Step 3](#step-3:-compile-a-smart-contract).
### 2.1 Setup and run a create-wallet script
Import the developer-controlled wallets SDK and initialize the client. You will
require your API key and Entity Secret for this. Note that your API key and
Entity Secret are sensitive credentials. Do not commit them to Git or share them
publicly. Store them securely in environment variables or a secrets manager.\
Developer-controlled wallets are created in a wallet set, which is the source
from which individual wallet keys are derived.
```typescript NodeJS theme={null}
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
const client = initiateDeveloperControlledWalletsClient({
apiKey: "",
entitySecret: "",
});
// Create a wallet set
const walletSetResponse = await client.createWalletSet({
name: "WalletSet 1",
});
console.log("Created WalletSet", walletSetResponse.data?.walletSet);
// Create a wallet on Arc Testnet
const walletsResponse = await client.createWallets({
blockchains: ["ARC-TESTNET"],
count: 1,
walletSetId: walletSetResponse.data?.walletSet?.id ?? "",
});
console.log("Created Wallets", walletsResponse.data?.wallets);
```
```python Python theme={null}
from circle.web3 import utils
from circle.web3 import developer_controlled_wallets
client = utils.init_developer_controlled_wallets_client(
api_key="",
entity_secret=""
)
wallet_sets_api = developer_controlled_wallets.WalletSetsApi(client)
wallets_api = developer_controlled_wallets.WalletsApi(client)
# Create a wallet set
wallet_set = wallet_sets_api.create_wallet_set(
developer_controlled_wallets.CreateWalletSetRequest.from_dict({
"name": "Wallet Set 1"
})
)
# Create a wallet on Arc Testnet
wallet = wallets_api.create_wallet(
developer_controlled_wallets.CreateWalletRequest.from_dict({
"blockchains": ["ARC-TESTNET"],
"count": 1,
"walletSetId": wallet_set.data.wallet_set.actual_instance.id
})
)
```
If you are not using the developer-controlled wallets SDK, you can call the API
directly as well. You will need to make 2 requests, one to create the wallet set
and one to create the wallet. Make sure to replace the
[entity secret ciphertext](https://developers.circle.com/wallets/dev-controlled/entity-secret-management#entity-secret-ciphertext)
and idempotency key.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/developer/walletSets \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '
{
"entitySecretCiphertext": "",
"idempotencyKey": "",
"name": "WalletSet 1"
}
'
```
The wallet set ID is required for creating the wallet.
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/developer/wallets \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "",
"blockchains": [
"ARC-TESTNET"
],
"entitySecretCiphertext": "",
"walletSetId": "",
"accountType": "EOA",
"count": 1,
]
}
'
```
You should end up with a new developer-controlled wallet, and the response will
look something like this:
```json theme={null}
[
{
"id": "a2f67c91-b7e3-5df4-9c8e-42bbd51a9fcb",
"state": "LIVE",
"walletSetId": "5c3e9f20-8d4b-55a1-a63b-c21f44de8a72",
"custodyType": "DEVELOPER",
"refId": "",
"name": "",
"address": "0x9eab451f27dca39bd3f5d76ef28c86cc0b3a72df",
"blockchain": "ARC-TESTNET",
"accountType": "EOA",
"updateDate": "2025-11-07T01:35:03Z",
"createDate": "2025-11-07T01:35:03Z"
}
]
```
### 2.3 Fund the wallet with test USDC
Obtain some testnet USDC for executing transactions like making transfers and
paying gas fees for those transactions. Circle's
[Testnet Faucet](https://faucet.circle.com/) provides testnet USDC and can be
used once per hour to obtain additional USDC.
### 2.4 Check the wallet's balance
You can check your wallet's balance from the
[Developer Console](https://console.circle.com/wallets/dev/wallets) or
programmatically by making a request to
[`GET /wallets/{id}/balances`](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-balance)
with the wallet ID of the wallet you created.
```ts NodeJS theme={null}
const response = await client.getWalletTokenBalance({
id: "",
});
```
```py Python theme={null}
try:
wallet_balance = wallets_api.list_wallet_balance(id="")
print(wallet_balance.json())
except developer_controlled_wallets.ApiException as e:
print("Exception when calling WalletsApi->list_wallet_balance: %s\n" % e)
```
```shell cURL theme={null}
curl --request GET \
--url 'https://api.circle.com/v1/w3s/wallets/{}/balances' \
--header 'accept: application/json' \
--header 'authorization: Bearer '
```
## **Step 3: Compile a smart contract**
In this section, you will compile and deploy a minimal smart contract for an
onchain payment inbox using Contracts. Users pay by approving and depositing
USDC, payments are recorded via events, and the owner can withdraw the
accumulated balance.
This contract is intentionally minimal and for learning purposes only. Smart
contracts that manage real funds typically require additional security
patterns, testing, and audits, and often rely on community-reviewed libraries
such as [OpenZeppelin](https://www.openzeppelin.com/).
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract MerchantTreasuryUSDC {
address public immutable owner;
IERC20 public immutable usdc;
event PaymentReceived(address indexed sender, uint256 amount);
event FundsWithdrawn(address indexed to, uint256 amount);
constructor(address _owner, address _usdc) {
require(_owner != address(0), "Invalid owner");
require(_usdc != address(0), "Invalid USDC");
owner = _owner;
usdc = IERC20(_usdc);
}
function deposit(uint256 amount) external {
require(amount > 0, "Invalid amount");
bool ok = usdc.transferFrom(msg.sender, address(this), amount);
require(ok, "USDC transferFrom failed");
emit PaymentReceived(msg.sender, amount);
}
function withdraw() external {
require(msg.sender == owner, "Unauthorized");
uint256 amount = usdc.balanceOf(address(this));
require(amount > 0, "No funds");
bool ok = usdc.transfer(owner, amount);
require(ok, "USDC transfer failed");
emit FundsWithdrawn(owner, amount);
}
function balance() external view returns (uint256) {
return usdc.balanceOf(address(this));
}
}
```
* `constructor(address _owner)`: Sets the treasury owner and the USDC token
address at deployment
* `receive() (payable)`: Transfers approved USDC from the caller into the
contract and emits `PaymentReceived(sender, amount)`
* `withdraw()`: Allows only the owner to withdraw the entire USDC balance; emits
`FundsWithdrawn(to, amount)`
* `balance() (view)`: Returns the contract's current USDC balance
### 3.1 Obtain ABI and bytecode from Remix IDE
1. Open the [Remix IDE](https://remix.ethereum.org/).
2. Create a new file in the contracts folder called `MerchantTreasury.sol`.
3. Copy and paste the Solidity code into the file, then click on the Compile
button.
4. Navigate to the Solidity Compiler tab from the left sidebar. Under Contracts,
make sure MerchantTreasuryUSDC (Merchant Treasury.sol) is selected. You
should see the option to copy the ABI and Bytecode. These values will be used
in the next step.
1. The compiler output is available under Compilation Details. For more
information on the Solidity compiler's outputs, see [using the
compiler](https://docs.soliditylang.org/en/stable/using-the-compiler.html).
2. The Application Binary Interface (ABI) is the standard way to interact
with contracts on an EVM from outside the blockchain and for
contract-to-contract interaction.
## Step 4: Deploy the smart contract
In this section, you will deploy the smart contract on Arc using the contract's
ABI and bytecode, which you have compiled in the previous step.
Import and initialize the Contracts SDK, then copy the ABI JSON and raw bytecode
over from Remix. Note that you need to append `0x` to the raw bytecode.
The `constructorParameters` correspond to the arguments encoded in the
contract's deployment bytecode. Since different contracts define different
constructors, these parameters vary based on the specific contract being
deployed. For this contract, the parameters are the wallet address of the owner
and the USDC token contract address on Arc Testnet.
```typescript NodeJS theme={null}
import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform";
const client = initiateSmartContractPlatformClient({
apiKey: "",
entitySecret: "",
});
const abiJson = PASTE_YOUR_ABI_JSON_HERE;
const bytecode = "0xPASTE_YOUR_BYTECODE_HERE";
const response = await client.deployContract({
name: "MerchantTreasury Contract",
description:
"Contract to receive payments and allow an owner to withdraw funds",
blockchain: "ARC-TESTNET",
walletId: "",
abiJson: JSON.stringify(abiJson, null, 2),
bytecode: bytecode,
constructorParameters: [
"", // Initial owner of the contract
"0x3600000000000000000000000000000000000000", // USDC contract address on Arc Testnet
],
fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});
console.log(response.data);
```
```python Python theme={null}
from circle.web3 import smart_contract_platform
from circle.web3 import utils
client = utils.init_smart_contract_platform_client(
api_key="",
entity_secret=""
)
api_instance = smart_contract_platform.DeployImportApi(client)
abi_json_str = """PASTE_YOUR_ABI_JSON_HERE"""
abi = json.loads(abi_json_str)
abi_json = json.dumps(abi)
request = smart_contract_platform.ContractDeploymentRequest.from_dict({
"name": 'MerchantTreasury Contract',
"description": 'Contract to receive payments and allow an owner to withdraw funds',
"blockchain": 'ARC-TESTNET',
"walletId": '',
"abiJson": abi_json,
"bytecode": "0xPASTE_YOUR_BYTECODE_HERE",
"constructorParameters": ['', '0x360000000000000000000000000000000000000'], # owner address and USDC contract address on Arc Testnet
"feeLevel": 'MEDIUM',
})
response = api_instance.deploy_contract(
contract_deployment_request=request
)
print(response.json())
```
```shell cURL theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/contracts/deploy \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey": "",
"name": "MerchantTreasury Contract",
"description": "Contract to receive payments and allow an owner to withdraw funds",
"walletId": "",
"blockchain": "ARC-TESTNET",
"abiJson": "PASTE_YOUR_ABI_JSON_HERE",
"bytecode": "0xPASTE_YOUR_BYTECODE_HERE",
"constructorParameters": ["", "0x36000000000000000000000000000000000000"],
"feeLevel": "MEDIUM",
"entitySecretCiphertext": ""
}
'
```
After running the script successfully, you should receive a response object that
looks like this:
```shell theme={null}
{
contractId: 'xxxxxxxx-xxxx-7xxx-8xxx-xxxxxxxxxxxx',
transactionId: 'xxxxxxxx-xxxx-5xxx-axxx-xxxxxxxxxxxx'
}
```
You can check the status of the deployment from the
[Developer Console](https://console.circle.com/smart-contracts/contracts) or run
`getContract` from the SDK directly.
```ts NodeJS theme={null}
const response = await circleContractSdk.getContract({
id: "",
});
```
```py Python theme={null}
api_instance = smart_contract_platform.ViewUpdateApi(client)
response = api_instance.get_contract(id="")
print(response.json())
```
```shell cURL theme={null}
curl --request GET \
--url https://api.circle.com/v1/w3s/contracts/{CONTRACT_ID} \
--header 'Authorization: Bearer '
```
Once your contract is deployed, you will be able to interact with it from your
application and mint new NFTs with it. You should be able to see it from the
console and on the [Arc Testnet Explorer](https://testnet.arcscan.app/).
# Quickstart: Event Monitoring for Smart Contracts
Source: https://developers.circle.com/contracts/scp-event-monitoring
In this guide you'll set up real-time push notifications for specific Events
that occur in your smart contracts. You can then use those events to trigger
important functionality in your application.
## Prerequisites
Before you begin:
* [Create an API key](/contracts/create-api-key) in the Circle Console
Perform the steps below:
1. [Step 1. Configure Your Webhook for Notifications](/contracts/scp-event-monitoring#step-1-configure-your-webhook-for-notifications)
2. [Step 2. Import Your Smart Contract](/contracts/scp-event-monitoring#step-2-import-your-smart-contract)
3. [Step 3. Create an Event Monitor](/contracts/scp-event-monitoring#step-3-create-an-event-monitor)
4. [Step 4. Fetch Event History](/contracts/scp-event-monitoring#step-4-fetch-event-history)
### Step 1. Configure Your Webhook for Notifications
To receive notifications from Circle, you must expose a publicly accessible
subscriber endpoint on your side. This endpoint should handle POST requests over
HTTPS.
For more information on setting up a webhook, refer to the
[Set Up Webhooks](/wallets/webhook-notifications), and optionally watch the
following video about Webhook Configurations.
## 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 Developer 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
Fast, cost-effective, and compliant global payments
Circle Payments Network (CPN) is a next-generation payment infrastructure
designed to reduce reliance on multiple intermediaries while enhancing
transparency, security, and speed. Using onchain primitives and built-in
compliance capabilities, CPN delivers near-instant cross-border settlement. CPN
allows participating financial institutions to rapidly 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 using Transactions V1 or V2.
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.
# 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).
# 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.
# 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
CPN supports two versions of onchain transactions: V1 and an optional upgrade,
V2, which enables secure and compliant stablecoin settlement across all
blockchains that
[CPN supports](/cpn/references/blockchains/supported-blockchains). Both deliver
the same core payment capabilities but differ in how gas fees, signing, and
orchestration are managed.
## 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.
Unlike Transactions V1, which uses EIP-3009 for token approvals, the payment
settlement contract uses the `Permit2` contract, Uniswap's universal token
approval system. `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, and is only a
requirement for Transactions V1. 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 |
Nonce handling requirements differ between CPN Transactions V1 and V2. V1
requires you to manage nonces directly; V2 provides unsigned transaction data
that includes nonce values. 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. If you use Transactions V1, your wallet also
manages nonce selection; V2 returns a complete unsigned payload.
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](/cpn/guides/webhooks/setup-webhook-notifications)
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 |
# 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 (see the API
reference and
[Notifications quickstart](/circle-mint/circle-apis-notifications-quickstart)):
```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/notifications-data-models) 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](/circle-mint/circle-apis-api-errors#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/notifications-data-models). 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](/circle-mint/circle-apis-api-errors#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 certificate is signed by a Circle-approved Certificate Authority listed in
the Circle-provided CA bundle file
* 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");
String caBundlePath = "ca_bundle.pem"; // CA bundle file provided by Circle
// ---------- 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 was signed by a Circle approved CA ----------
List caCerts = loadCABundle(caBundlePath);
boolean trusted = false;
for (X509Certificate caCert : caCerts) {
if (certificate.getIssuerX500Principal().equals(caCert.getSubjectX500Principal())) {
trusted = true;
System.out.println("✅ Certificate is signed by trusted CA: " + caCert.getSubjectX500Principal());
break;
}
}
if (!trusted) {
System.out.println("❌ Certificate verification failed. CA is not trusted.");
System.exit(1);
}
// ---------- 3. 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);
}
// ---------- 4. 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](/cpn/references/errors/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.
There are two versions of the Transactions API: V1 and V2. This guide provides
information on both versions. Note that your quote must match the version of the
API that you are using.
## Prerequisites
Before you begin, ensure that you have:
* Created a quote through the CPN API. If you are following the Transactions V2
example, your quote should be created with the `transactionVersion` parameter
set to `VERSION_2`.
* USDC in your sender wallet
* (Transactions V1 only) Native tokens in your sender wallet
* (Transactions V2 on 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.
**Note:** For Transactions V2, 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](/api-reference/cpn/cpn-platform/create-transaction)
endpoint to get an unsigned message object. Note that transaction objects differ
between EVM blockchains and Solana.
For Transactions V2, call the
[create transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint to get an unsigned message object. Transactions V2 supports EVM chains
only.
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 it in accordance with
[EIP-712](https://eips.ethereum.org/EIPS/eip-712). Next, you construct an
[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) transaction from the signed
data:
1. **Extract the signature**. Extract the signature components (`v`, `r`, `s`)
from the signed typed data.
2. **Encode the function call**. Using the ERC-20 smart contract's ABI for the
`TransferWithAuthorization` function, encode the function call with the
required parameters: sender address (`from`), recipient address (`to`),
`validAfter`, `validBefore`, `nonce`, and the signature components, `v`, `r`,
`s`. This encoding creates the data field for the raw transaction.
3. **Construct the transaction object**. Build a raw transaction object that
includes the target contract address (USDC), the encoded function call data,
and other necessary parameters such as nonce, gas limit, max fee per gas, max
priority fee per gas, and chain ID.
4. **Serialize the transaction**. Serialize the transaction object into the
proper RLP-encoded format so that it can be signed.
The following is an example of the transaction object in
[EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) format, for EIP-3009
`TransferWithAuthorization` contract execution.
```json JSON theme={null}
{
"to": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"value": "0x0",
"gasLimit": "0x5208",
"maxPriorityFeePerGas": "0x59682f00",
"maxFeePerGas": "0x59682f00",
"nonce": "0x8",
"chainId": 1,
"type": "0x2",
"data": "0xe3ee160e000000000000000000000000a9d56270e9fd76be802ac4d45ef4be4322fdadbc0000000000000000000000006840c9f894b6b8264292e22b8abb2c57ae3946a700000000000000000000000000000000000000000000000000000000000f424000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067ca8f131e790e4b5e8d2f801f12bdd8e8a9fcab490305f17a59a1620549791985617c36000000000000000000000000000000000000000000000000000000000000001cc8973666e8460a153a5a073a7a2878a4e6f42be09ffe012a04d342f2a729019b769e46ab7fefb0dadf8a6d0dd83a687d6dfcdfaf1c56af190eaaff8252e5929e"
}
```
Sign this transaction data and move on to the next step.
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
For V1, the transfer data for Solana follows Solana's `Ed2559` transaction
format. After confirming the unsigned data, a transaction object is constructed
with the transfer data. You sign the transaction object and submit it to the
API.
To sign the transaction object, you can deserialize the `messageToBeSigned`
field from the
[POST /v1/cpn/payments/:paymentId/transactions](/api-reference/cpn/cpn-platform/create-transaction)
endpoint, and sign it using a Solana library with your wallet key pair.
```typescript theme={null}
import {
Keypair,
Transaction,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import bs58 from "bs58";
function signTransaction(messageToBeSigned: any, keypair: Keypair): string {
// 1. Create a new Transaction
const transaction = new Transaction();
// 2. Set recentBlockhash and feePayer
transaction.recentBlockhash = messageToBeSigned.recentBlockhash;
transaction.feePayer = new PublicKey(messageToBeSigned.feePayer);
// 3. Add all instructions
for (const instruction of messageToBeSigned.instructions) {
const keys = instruction.keys.map((key: any) => ({
pubkey: new PublicKey(key.pubkey),
isSigner: key.isSigner,
isWritable: key.isWritable,
}));
const programId = new PublicKey(instruction.programId);
// Handle instruction data - can be array of numbers or base64 string
let data: Buffer;
if (Array.isArray(instruction.data)) {
data = Buffer.from(instruction.data);
} else if (typeof instruction.data === "string") {
data = Buffer.from(instruction.data, "base64");
} else {
throw new Error(`Unexpected data format: ${typeof instruction.data}`);
}
transaction.add(
new TransactionInstruction({
keys,
programId,
data,
}),
);
}
// 4. Sign with keypair
transaction.sign(keypair);
// 5. Serialize signed transaction, which you can then submit to the
// POST /v1/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.
**Note:** Once signed, Solana transaction objects expire after one minute, so
you should submit it immediately.
For V2, 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](/api-reference/cpn/cpn-platform/submit-transaction)
endpoint to submit the signed transaction data to CPN. For Transactions V2, use
the
[submit transaction V2](/api-reference/cpn/cpn-platform/submit-transaction-v2)
endpoint. 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. For example, in a V1 transaction,if there was not enough gas to
cover the transaction, you could deposit additional gas tokens to the wallet,
and try again. In a V2 transaction, this is less likely to be an issue as gas
fees in native tokens are paid from a Circle-controlled wallet. 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.
**Note:** 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, which is likely the case if you are migrating from
Transactions V1 to V2, you can 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
**Note:** This example is for a Circle Wallets developer-controlled wallet on
generic EVM blockchains (for example, `EVM`, `EVM-TESTNET`). If you are getting
started with Transactions V2 instead of migrating from Transactions V1, Circle
recommends that you use 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.
```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 typical Transactions V2 sandbox testing. For Transactions
V1 on EVM testnet, use `EVM-TESTNET` instead. 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](/wallets/webhook-notifications),
[Webhook notification flows](/wallets/webhook-notification-flows), and
[Create subscription](/api-reference/wallets/common/create-subscription)
(`POST /v2/notifications/subscriptions`). Common event types include
`transactions.inbound` and `transactions.outbound`.
### 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. For Transactions V1 on EVM, ensure you also have testnet native
gas tokens (check the faucet for your target 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 webhook notifications](/cpn/guides/webhooks/setup-webhook-notifications)
(CPN payment events)
* [Webhook events](/cpn/references/webhooks/webhook-events)
* [Entity secret management](/wallets/dev-controlled/entity-secret-management)
# Set up Webhook Notifications
Source: https://developers.circle.com/cpn/guides/webhooks/setup-webhook-notifications
During the payment process, CPN sends notifications using webhooks. These
asynchronous notifications inform the entities on the progress of the payment
and any required actions they must take.
This guide demonstrates how to subscribe to CPN notifications as an OFI
integrator. See [Webhook Events](/cpn/references/webhooks/webhook-events) for a
complete list of the notifications that are sent over webhooks.
## Steps
Use the following steps to set up a webhook endpoint and subscribe to
notifications using the CPN API.
### Step 1: Set up a subscriber endpoint
To receive webhook notifications, you must expose a publicly accessible
subscriber endpoint. The endpoint must be able to handle both `HEAD` and `POST`
requests over HTTPS.
**Note:** During OFI integration, Circle works directly with you to configure
this endpoint.
For testing purposes, you can create an endpoint using
[webhook.site](https://webhook.site).
### Step 2: Subscribe to notifications
Set up an endpoint to receive notifications instead of polling for event updates
using the `/v2/subscriptions` endpoint.
```shell Shell theme={null}
curl --request POST \
--url https://api.circle.com/v2/cpn/notifications/subscriptions \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}:' \
--header 'Content-type: application/json' \
--data '
{
"endpoint": "${YOUR_WEBHOOK_ENDPOINT}",
"name": "Test OFI",
"enabled": true,
"notificationTypes": ["*"]
}
'
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "1609aa1c-510a-448d-b9b9-3a13566ff922",
"name": "Test OFI",
"endpoint": "https://webhook.site/1fde07a9-8974-42bd-a273-943ffdf0e7d6",
"enabled": true,
"createDate": "2025-05-08T16:20:02.825689Z",
"updateDate": "2025-05-08T16:20:02.825689Z",
"notificationTypes": ["*"],
"restricted": false
}
}
```
Webhook notifications are digitally signed with an asymmetric key. You can use
information in the webhook header to verify that the content of the webhook is
legitimate. For more information, see
[How-to: Verify Webhook Signatures](/cpn/guides/webhooks/verify-webhook-signatures).
# Verify Webhook Signatures
Source: https://developers.circle.com/cpn/guides/webhooks/verify-webhook-signatures
Every webhook notification sent by CPN is digitally signed with an asymmetric
key. This guide demonstrates how to use the key and signature to verify that a
webhook notification was sent by Circle. Validating webhooks in this way can
reduce the risk of person-in-the-middle attacks on your subscriber endpoint.
## Steps
Use the following steps to verify the Circle signature on a webhook
notification.
### Step 1: Get the digital signature and ID of the notification
Every webhook notification is digitally signed with an asymmetric key. The
asymmetric key is random for each webhook, so you must perform this full
authentication flow to validate the key. This signature is available in the
header of the message. Each message contains the following headers:
* `X-Circle-Signature`: the digital signature generated by Circle
* `X-Circle-Key-Id`: the public key ID in UUID format
Extract those values from the header of the webhook message.
### Step 2: Get the public key and encryption algorithm
Using the `X-Circle-Key-Id` value, query the
[`/v2/cpn/notifications/publicKey/{keyId}`](/api-reference/cpn/common/get-notification-signature)
endpoint.
```shell Shell theme={null}
curl --request GET \
--url "https://api.circle.com/v2/cpn/notifications/publicKey/${PUBLIC_KEY_ID}" \
--header "Accept: application/json" \
--header "authorization: Bearer ${YOUR_API_KEY}"
```
**Response**
```json JSON theme={null}
{
"data": {
"id": "879dc113-5ca4-4ff7-a6b7-54652083fcf8",
"algorithm": "ECDSA_SHA_256",
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg==",
"createDate": "2023-06-28T21:47:35.107250Z"
}
}
```
**Note:** To avoid making multiple requests to the public key endpoint, you
should cache the public key associated with a given public key ID.
### Step 3: Verify the signature
Use the public key and the specified algorithm from the response in step 2,
along with the `X-Circle-Signature` value, to verify the integrity of the
webhook's payload.
The following Python code demonstrates how to verify the `X-Circle-Signature`
value:
```python Python theme={null}
import base64
import json
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
# Load the public key from the base64 encoded string
public_key_base64 = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESl76SZPBJemW0mJNN4KTvYkLT8bOT4UGhFhzNk3fJqf6iuPlLQLq533FelXwczJbjg2U1PHTvQTK7qOQnDL2Tg=="
public_key_bytes = base64.b64decode(public_key_base64)
public_key = serialization.load_der_public_key(public_key_bytes)
# Load the signature you want to verify
signature_base64 = "MEQCIBlJPX7t0FDOcozsRK6qIQwik5Fq6mhAtCSSgIB/yQO7AiB9U5lVpdufKvPhk3cz4TH2f5MP7ArnmPRBmhPztpsIFQ=="
signature_bytes = base64.b64decode(signature_base64)
# Load and format the message you want to verify
message = "{\"subscriptionId\":\"00000000-0000-0000-0000-000000000000\",\"notificationId\":\"00000000-0000-0000-0000-000000000000\",\"notificationType\":\"webhooks.test\",\"notification\":{\"hello\":\"world\"},\"timestamp\":\"2024-01-26T18:22:19.779834211Z\",\"version\":2}"
message_bytes = message.encode(encoding="utf-8")
# Verify the signature
try:
public_key.verify(
signature_bytes,
message_bytes,
ec.ECDSA(hashes.SHA256())
)
print("Signature is valid.")
except InvalidSignature:
print("Signature is invalid.")
```
**Tip:** Ensure that the webhook payload that you input in the `message` field
is a properly formatted JSON string. Invalid JSON causes verification failure.
# CPN 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 sub-wallets, 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 sub-wallet; send payouts from each sub-wallet or the main
wallet when enabled.
* **Merchant sub-wallets:** 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 share it with Circle. Circle
creates sub-wallets for the merchants/customers after KYB approval.
* **Intermediary:** Circle doesn't have to onboard your underlying merchants
or customers. You create sub-wallets 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 sub-wallets, wires, stablecoin payouts,
reports, and balance views. Circle configures these roles during onboarding.
Creating merchant sub-wallets depends on the onboarding process you agree on
with Circle.
* **Direct onboarding:** You collect end-merchant Know Your Business (KYB)
information and share it with Circle through a secure offline channel. Circle
creates sub-wallets for those merchants on your behalf.
* **Intermediary model:** Use the
[accounts API](/api-reference/cpn/managed-payments/accounts/create-account)
to create the sub-wallets you need for your merchants. Sub-wallets 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 sub-wallet 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 sub-account wallets for merchants. List and get stablecoin accounts for self and merchant sub-wallets. |
| [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. |
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.
Sub-wallets, 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 sections below 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
sub-wallet 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 sub-wallet 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.
Sub-wallets 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 sub-wallets and the main wallet interact during payouts, see
[Sub-Wallet Architecture](/cpn/managed-payments/concepts/sub-wallet-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 sub-wallet setup and balance tracking, see
[Sub-Wallet Architecture](/cpn/managed-payments/concepts/sub-wallet-architecture).
# Sub-Wallet Architecture
Source: https://developers.circle.com/cpn/managed-payments/concepts/sub-wallet-architecture
Sub-wallets, your main wallet, and how they work together in Managed Payments
A Managed Payments sub-wallet is a separate ledger for a merchant or segment. It
belongs to your Circle Managed Payments Account for your setup.
* Most teams create one sub-wallet per merchant or segment. Then USDC balances
and activity stay easy to trace in your books and in Circle reports.
* Your setup has a main wallet too. USDC can live in the main wallet only, in
sub-wallets only, or across both. It depends on how you fund and pay out.
Together these wallets let you split balances for books and reports while still
using shared funding or pooled sends when your setup allows.
## Wallets structure and typical flows
The following figure is a simplified model. 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[Sub-wallet A]
SB[Sub-wallet 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
sub-wallet before payout.
## Accounts API
If you use the **Direct** onboarding model, you can skip this section. In that
model, Circle creates sub-wallets on your behalf.
Use the Stablecoin Accounts API to create and inspect the stablecoin account
that backs each merchant sub-wallet.
* [Create a managed payments intermediary account](/api-reference/cpn/managed-payments/accounts/create-account)
when you add a merchant sub-wallet in the Intermediary onboarding model.
* [List accounts](/api-reference/cpn/managed-payments/accounts/list-accounts) to
page through sub-wallets and see each one's balances and metadata.
* [Get an account](/api-reference/cpn/managed-payments/accounts/get-account) to
load one sub-wallet's stablecoin account by `accountId`, including balances
and metadata.
Sub-wallet activity in API requests and responses references these account and
wallet IDs. Circle grants role-based API access during onboarding; the endpoints
available to your API keys depend on the roles assigned to 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
sub-wallets during onboarding. In both cases, you reference the same `accountId`
values for wires, payouts, and payins.
## Funding and pooled balances
Depending on your setup, USDC can be funded and spent across the main wallet and
sub-wallets in several ways:
* Wires can credit one sub-wallet 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 sub-wallet before they go onchain. Terms
follow your agreement with Circle.
* Payouts usually debit a sub-wallet balance. If your integration supports it,
you can fund payouts from the main wallet instead of from each sub-wallet.
That helps when you send a lot and don't want USDC in every sub-wallet first.
Use that path only when your setup supports it.
## Payins and payouts
Handle payins, debits, and reconciliation per sub-wallet:
* **Payins (receiving USDC):** Each sub-wallet 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
sub-wallet to debit when you create a payout.
| 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 |
# 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 sub-wallet.
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.
* Sub-wallets 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
[Stablecoin 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. Follow the
[notifications quickstart](/circle-mint/circle-apis-notifications-quickstart).
* (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
CAPI-->>Server: Webhook payment intent complete 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).
Create a payment intent:
```curl cURL theme={null}
curl --location --request POST 'https://api-sandbox.circle.com/v1/paymentIntents' \
--header 'X-Requested-Id: ${GUID}' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
"idempotencyKey": "17607606-e383-4874-87c3-7e46a5dc03dd",
"settlementCurrency": "USD",
"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 = {
settlementCurrency: "USD",
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"
}
}
```
```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 the
[notifications quickstart](/circle-mint/circle-apis-notifications-quickstart).
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-Requested-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 via `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 via 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.
## 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 via 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-Requested-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-Requested-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 sub-wallet 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 sub-wallet wallet ID for `source` when you don't pay from the main wallet
alone. Read [Managed Payments](/cpn/managed-payments) and
[Sub-wallet architecture](/cpn/managed-payments/concepts/sub-wallet-architecture).
* (Optional) A webhook endpoint for address book and payout events. Use the
[notifications quickstart](/circle-mint/circle-apis-notifications-quickstart).
## 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).
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 sub-wallet.
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.
**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 sub-wallet 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"
}
}'
```
```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,
},
};
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"
}
}
```
# Webhook Events
Source: https://developers.circle.com/cpn/managed-payments/references/webhook-events
Reference for Managed Payments webhook event types, payloads, and the v1 notification subscription system.
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).
* Subscription endpoint: `POST /v1/notifications/subscriptions`
* Setup:
[Circle APIs notifications quickstart](/circle-mint/circle-apis-notifications-quickstart)
All Managed Payments webhook payloads use the following v1 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`. |
| `customAttributes` | object | Contains `clientId`. |
| `` | object | Event-specific data. Key name varies by event type. |
## Event types
| `notificationType` | Resource key | Trigger |
| ----------------------- | ---------------------- | ----------------------------------------------------------------------- |
| `paymentIntents` | `paymentIntent` | Payment intent created, deposit address ready, or payment received |
| `payments` | `payment` | Payment status changes (for example, `pending` or `paid`) |
| `addressBookRecipients` | `addressBookRecipient` | Recipient status changes (for example, `pending` or `active`) |
| `payout` | `payout` | Payout status changes (for example, `pending`, `complete`, or `failed`) |
| `creditTransfer` | `creditTransfer` | Credit transfer status changes (`requested`, `disbursed`, or `paid`) |
## Payment intent events
The `paymentIntents` notification fires when a payment intent is created, when a
blockchain deposit address becomes available, or when a payment is received
against the intent.
Status values observed in `timeline[].status`: `created`, `active`
```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` notification fires when a payment status changes.
Status values: `pending`, `paid`
```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` notification fires when a recipient's status
changes.
Status values: `pending`, `active`
```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 `payout` notification fires when a payout status changes.
Status values: `pending`, `complete`, `failed`
```json Example payout payload 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"
}
}
```
## Credit transfer events
The `creditTransfer` notification fires when a credit transfer status changes. A
credit transfer represents a draw from a line of credit.
Status values: `requested` (disbursement requested), `disbursed` (funds
transferred to wallet), `paid` (fully repaid; fees invoiced at month end)
```json Example creditTransfer payload theme={null}
{
"clientId": "a49f9b1d-75e0-44a9-b8d2-4293b3f11ebd",
"notificationType": "creditTransfer",
"version": 1,
"customAttributes": {
"clientId": "a49f9b1d-75e0-44a9-b8d2-4293b3f11ebd"
},
"creditTransfer": {
"data": {
"id": "0c206348-1252-4c33-8618-5d85e322a0a3",
"amount": {
"amount": "50.00",
"currency": "USD"
},
"status": "disbursed",
"outstanding": {
"amount": "50.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 following table documents every field in the `creditTransfer.data` object.
| Field | Type | Description |
| --------------- | -------------------------------------- | ---------------------------------------------------------------------------- |
| `id` | string (UUID) | Unique identifier for the credit transfer. |
| `amount` | Amount object (`{ amount, currency }`) | Total amount drawn from the line of credit. |
| `status` | enum: `requested`, `disbursed`, `paid` | Current status of the credit transfer. |
| `outstanding` | Amount object (`{ amount, currency }`) | Remaining balance to repay. Omitted when status is `requested`. |
| `dueDate` | ISO 8601 timestamp | When repayment is due. Omitted when status is `requested`. |
| `disbursedDate` | ISO 8601 timestamp | When funds were disbursed to the wallet. Omitted when status is `requested`. |
| `paidDate` | ISO 8601 timestamp | When the credit transfer was fully repaid. Omitted unless status is `paid`. |
| `createDate` | ISO 8601 timestamp | When the credit transfer was created. |
| `updateDate` | ISO 8601 timestamp | When the credit transfer was last updated. |
# 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 V1 and 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 cover both Transactions V1 and V2 on EVM
blockchains; where the two versions differ, each step provides version-specific
guidance. 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 (`EVM-TESTNET` for
Transactions V1, `ETH-SEPOLIA` for Transactions V2 when using testnet).
* Funded the operational wallet with enough **USDC** (and, for Transactions V1
on EVM, enough **native gas**). For sandbox you can use the
[Circle faucet](https://faucet.circle.com) and an
[EVM testnet ETH faucet](https://cloud.google.com/application/web3/faucet/ethereum).
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
* (If using Transactions V2) 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](/cpn/guides/webhooks/setup-webhook-notifications)
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. For Transactions
V2, 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.
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"
}
]
}
```
```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"
}
'
```
**Response**
```json theme={null}
{
"data": [
{
"id": "922a06cd-ff1e-4ee4-840e-54006893fd1a",
"paymentMethodType": "SPEI",
"blockchain": "ETH-SEPOLIA",
"senderCountry": "US",
"destinationCountry": "MX",
"createDate": "2025-03-28T16:21:47.089081899Z",
"quoteExpireDate": "2025-03-28T16:22:45.713598Z",
"cryptoFundsSettlementExpireDate": "2025-03-28T18:21:45.713615Z",
"sourceAmount": {
"amount": "10.000000",
"currency": "USDC"
},
"destinationAmount": {
"amount": "200.23",
"currency": "MXN"
},
"fiatSettlementTime": {
"min": "1",
"max": "12",
"unit": "HOURS"
},
"exchangeRate": {
"rate": "20.040000",
"pair": "USDC/MXN"
},
"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"
}
}
]
},
"senderType": "INDIVIDUAL",
"recipientType": "INDIVIDUAL",
"certificate": {
"id": "201c52fc-8866-44cf-a2e2-3ceae098381c",
"certPem": "LS0t...",
"domain": "api.circle.com",
"jwk": {
"kty": "EC",
"crv": "P-256",
"kid": "263521881931753643998528753619816524468853605762",
"x": "YdjOeAmlNfWV0xIryFAivcp9of21s0c-JhyGEOINV2Y",
"y": "n621ve_OV_p3jdocxtNkAk4uaKcYR2XWYUu1NMzBei8"
}
}
}
]
}
```
## 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 V1 Encryption Quickstart
This script demonstrates how to:
1. Parse V1 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 and encrypted sender and receiver information. The
endpoint returns a unique payment ID and the initial status of the payment.
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}",
"blockchain" : "ETH-SEPOLIA",
"reasonForPayment" : "PMT001",
"customerRefId" : "123c7442-e843-4afa-bfad-35f50636d35b",
"refCode" : "7b479c5a-3684-4423-9fc6-f7c890c0e816",
"useCase" : "B2B"
}
'
```
**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": "B2B_INVOICE_PAYMENT",
"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
[`/payments/{paymentId}/transactions`](/api-reference/cpn/cpn-platform/create-transaction)
endpoint with the payment ID from the previous step, and other
transaction-related parameters. Note that if you are using Transactions V2, you
should use the
[create transaction V2](/api-reference/cpn/cpn-platform/create-transaction-v2)
endpoint. 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": {}
}
}
```
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/cpn/payments/:paymentId/transactions \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"idempotencyKey" : "${RANDOM_UUID}",
"senderAccountType": "EOA",
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "8e0cc03f-799c-4971-ba41-6b790b4f9548",
"status": "CREATED",
"paymentId": "0a6973af-3089-4265-812b-0f68a426a4d8",
"expireDate": "2025-04-01T17:28:25.198159Z",
"senderAddress": "0x140f52a9D27764a51032ebDff7E6352D1640cbfd",
"senderAccountType": "EOA",
"blockchain": "ETH-SEPOLIA",
"amount": {
"amount": "10.000000",
"currency": "USDC"
},
"destinationAddress": "0x6e87cdf0b9d2d96232f5c605526cb0e89db7387a",
"estimatedFee": {
"type": "EIP1559",
"payload": {
"gasLimit": "150000",
"maxFeePerGas": "4829089726",
"maxPriorityFeePerGas": "2000000000"
}
},
"messageType": "EIP3009",
"messageToBeSigned": {
"domain": {
"chainId": "11155111",
"name": "USDC",
"verifyingContract": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"version": "2"
},
"message": {
"from": "0x140f52a9D27764a51032ebDff7E6352D1640cbfd",
"nonce": "0x75cc053bfcdedd359bfdaaa560fc0c7d3899097dcf6396e65b029df3b1e05a0e",
"to": "0x6e87cdf0b9d2d96232f5c605526cb0e89db7387a",
"validAfter": "1743519573",
"validBefore": "1743527605",
"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"
}
]
}
}
}
}
```
### 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"
}
```
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"
}
```
Create a file called `cpn_signature.py` and add the following code to it,
replacing `circle_signature` with the signature returned from the endpoint, and
replacing the message object values with the corresponding values from the
response in
[step 3.1](/cpn/quickstarts/integrate-with-cpn-ofi#3-1-initiate-the-onchain-transaction).
```python Python theme={null}
from web3 import Web3
from eth_utils import keccak, to_hex
from hexbytes import HexBytes
from eth_abi import encode
def get_function_selector(signature: str) -> str:
"""Return 4-byte function selector from signature."""
hash_bytes = keccak(text=signature)
return to_hex(hash_bytes[:4])
def encode_transfer_with_authorization(
from_address: str,
to_address: str,
value: int,
valid_after: int,
valid_before: int,
nonce: str,
v: int,
r: str,
s: str
) -> str:
"""Encode callData for transferWithAuthorization (EIP-3009 USDC)."""
types = [
"address", "address", "uint256", "uint256", "uint256",
"bytes32", "uint8", "bytes32", "bytes32"
]
args = [
Web3.to_checksum_address(from_address),
Web3.to_checksum_address(to_address),
value,
valid_after,
valid_before,
HexBytes(nonce),
v,
HexBytes(r),
HexBytes(s)
]
encoded_args = encode(types, args)
selector = get_function_selector(f"transferWithAuthorization({','.join(types)})")
return selector + encoded_args.hex()
# === INPUT DATA ===
circle_signature = your_signature
message = {
"from": your_from_address,
"to": your_to_address,
"value": 10_000_000, # 10 USDC (6 decimals)
"validAfter": your_valid_after,
"validBefore": your_valid_before,
"nonce": your_nonce
}
# === SPLIT SIGNATURE ===
sig_bytes = Web3.to_bytes(hexstr=circle_signature)
r = Web3.to_hex(sig_bytes[0:32])
s = Web3.to_hex(sig_bytes[32:64])
v = sig_bytes[64]
if v < 27:
v += 27 # Normalize v for Ethereum
# === ENCODE CALL DATA ===
call_data = encode_transfer_with_authorization(
from_address=message["from"],
to_address=message["to"],
value=message["value"],
valid_after=message["validAfter"],
valid_before=message["validBefore"],
nonce=message["nonce"],
v=v,
r=r,
s=s
)
print(f"✅ Final Call Data:\n{call_data}")
```
Next, you must create and sign the raw transaction using the
[`/sign-transaction`](/api-reference/wallets/developer-controlled-wallets/sign-transaction)
endpoint. Include the call data from the script as the `data` field and the
wallet ID from your Circle Wallet.
The raw transaction to be signed can be composed like the following example:
```json JSON theme={null}
{
"nonce": 1, // The nonce of the sender address, obtained via eth_getTransactionCount RPC call
"to": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // The target contract address, in this case, the USDC contract address
"gas": "150000", // The gas limit for the transaction, from `data.estimatedFee.payload.gasLimit` in the step 3.1 response.
"maxFeePerGas": "70000000000", // The max fee per gas, from `data.estimatedFee.payload.maxFeePerGas` in the step 3.1 response.
"maxPriorityFeePerGas": "3000000000", // The max priority fee per gas, from `data.estimatedFee.payload.maxPriorityFeePerGas` in the step 3.1 response.
"chainId": 11155111, // The chain ID, for Ethereum Sepolia testnet, the chain ID is 11155111
// The call data, obtained from the script in step 3.2
"data": "0xe3ee160e00000000000000000000000039fd73b03a01c6230b5e0d946e1960d79db44fd80000000000000000000000003eebc158f254838e2f6275b892e6a0621e3ea321000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000680bc9e000000000000000000000000000000000000000000000000000000000680be8863a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2000000000000000000000000000000000000000000000000000000000000001bd6960c1cc4c28482a3c96ea35e5c0cfe84f4e466f734de02023b15101c9735a04830dbfeeccd565705c1e8b92b3dd038d720130f5a3101bf43160be49e0f1651"
}
```
Use the following endpoint to sign the transaction:
```bash Shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/w3s/developer/sign/transaction \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '
{
"entitySecretCiphertext": "h8R0RizKx0KWX2wpZgfcUoSSFms0Qj/6pkGH3JSKaYJPhSNRl2GpPWba9ZilCRivI42Di9MRAxI5jsGjay1tyQcrasq3o0aC4jNvK6RH7f8DOnoeNQjmL4pFlLzp/R+NduNI/w/JH5rk84JhsAkOy5yXkMmGf9IkQbh4+381VojV3P8FCuVzsJDTI5KDWzzwMR3eExmQN8QmKlIIyxlAm1JSxhS5Y/9GqqMY+jtcSkxzkX965GzkGyODRo0gxPuUZCiES1lHSe9tkLJWs5AgvJ/2MVpaiDmcIXZJ3JNBw2EuAMp6uRiv3OiODrThgP44YSpvTPavfxDtAnxyw7ZrPSUeN8wX8RBsTpqxZaJvy4aJTCgnDjfvqfPcsg90UqhXYI0VBVU5489s89HHKw76AYp4Hz52Iu4FtsA6r2PidN4Cccp7Ges7gOde6vG36mOG0ODcxMwKyWcAkNdZYEPBQ1DK0c1s5dbNYImBHZ+EnfY0TlHroFOKYhMihrhkXTjCTL+HiSboJtoVGvOphmsyvoQMg0fzprJUVhOraH/soQkd61eulETFN6vJq8R5ODFeeBDlOkZny1Om2ZUd8tdobZDlVGiSZFUR4rPlntoUN5g/hPp8lB+25UN2KaIUiX3OR01EvRedA6Xr+kqzVsmgKmkNW1aFuOJFXEAXlMjR2fU=",
"transaction": "{\"nonce\":1,\"to\":\"0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238\",\"gas\":\"150000\",\"maxFeePerGas\":\"70000000000\",\"maxPriorityFeePerGas\":\"3000000000\",\"chainId\":11155111,\"data\":\"0xe3ee160e00000000000000000000000039fd73b03a01c6230b5e0d946e1960d79db44fd80000000000000000000000003eebc158f254838e2f6275b892e6a0621e3ea321000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000680bc9e000000000000000000000000000000000000000000000000000000000680be8863a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2000000000000000000000000000000000000000000000000000000000000001bd6960c1cc4c28482a3c96ea35e5c0cfe84f4e466f734de02023b15101c9735a04830dbfeeccd565705c1e8b92b3dd038d720130f5a3101bf43160be49e0f1651\"}",
"walletId": "${YOUR_CIRCLE_WALLET_ID}"
}
'
```
**Response**
```json JSON theme={null}
{
"signature": "0xe59d32312a920b6c63ad4c7344bb76d8e7cae2615f79f707649e325abea00a247cddec90138bb6790e68e01998fdf77efc9496a91b3b4b42e59fd0e8ad89d0bc00",
"signedTransaction": "0x02f9019583aa36a70184b2d05e0085104c533c00830249f0941c7d4b196cb0c7b01d743fbc6116a902379c723880b90124e3ee160e00000000000000000000000039fd73b03a01c6230b5e0d946e1960d79db44fd80000000000000000000000003eebc158f254838e2f6275b892e6a0621e3ea321000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000680bc9e000000000000000000000000000000000000000000000000000000000680be8863a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2000000000000000000000000000000000000000000000000000000000000001bd6960c1cc4c28482a3c96ea35e5c0cfe84f4e466f734de02023b15101c9735a04830dbfeeccd565705c1e8b92b3dd038d720130f5a3101bf43160be49e0f1651c080a0e59d32312a920b6c63ad4c7344bb76d8e7cae2615f79f707649e325abea00a24a07cddec90138bb6790e68e01998fdf77efc9496a91b3b4b42e59fd0e8ad89d0bc",
"txHash": "0xc1d5963f87e4a9035eae4e31fe7842a8bc1cd0ebf941d541c0b7ff37b4d1f5df"
}
```
### 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`.
Use the
[`/v1/cpn/payments/{paymentId}/transactions/{transactionId}/submit`](/api-reference/cpn/cpn-platform/submit-transaction)
endpoint to submit the transaction to be broadcast to the blockchain.
You should submit the `signedTransaction` you obtained from the
`/sign/transaction` endpoint in [step 3.2](#3-2-sign-the-onchain-transaction).
```shell theme={null}
curl --request POST \
--url https://api.circle.com/v1/cpn/payments/:paymentId/transactions/:transactionId/submit \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ${YOUR_API_KEY}' \
--header 'Content-type: application/json' \
--data '
{
"signedTransaction": "0x02f9019583aa36a70184b2d05e0085104c533c00830249f0941c7d4b196cb0c7b01d743fbc6116a902379c723880b90124e3ee160e00000000000000000000000039fd73b03a01c6230b5e0d946e1960d79db44fd80000000000000000000000003eebc158f254838e2f6275b892e6a0621e3ea321000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000680bc9e000000000000000000000000000000000000000000000000000000000680be8863a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2000000000000000000000000000000000000000000000000000000000000001bd6960c1cc4c28482a3c96ea35e5c0cfe84f4e466f734de02023b15101c9735a04830dbfeeccd565705c1e8b92b3dd038d720130f5a3101bf43160be49e0f1651c080a0e59d32312a920b6c63ad4c7344bb76d8e7cae2615f79f707649e325abea00a24a07cddec90138bb6790e68e01998fdf77efc9496a91b3b4b42e59fd0e8ad89d0bc"
}
'
```
**Response**
```json theme={null}
{
"data": {
"id": "1f3ccc13-69e3-4811-9648-755bc9aa26f4",
"status": "PENDING",
"paymentId": "fed8687a-d911-3682-a6f2-b2474a1016ba",
"expireDate": "2025-04-25T19:54:46.230217Z",
"senderAddress": "0x39fd73b03a01c6230b5e0d946e1960d79db44fd8",
"senderAccountType": "EOA",
"blockchain": "ETH-SEPOLIA",
"amount": {
"amount": "10.000000",
"currency": "USDC"
},
"destinationAddress": "0x3eebc158f254838e2f6275b892e6a0621e3ea321",
"estimatedFee": {
"type": "EIP1559",
"payload": {
"gasLimit": "150000",
"maxFeePerGas": "27514930294",
"maxPriorityFeePerGas": "2000000000"
}
},
"messageType": "EIP3009",
"messageToBeSigned": {
"domain": {
"chainId": "11155111",
"name": "USDC",
"verifyingContract": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"version": "2"
},
"message": {
"from": "0x39fd73b03a01c6230b5e0d946e1960d79db44fd8",
"nonce": "0x3a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2",
"to": "0x3eebc158f254838e2f6275b892e6a0621e3ea321",
"validAfter": "1745603040",
"validBefore": "1745610886",
"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"
}
]
}
},
"signedTransaction": "0x02f9019583aa36a70184b2d05e0085104c533c00830249f0941c7d4b196cb0c7b01d743fbc6116a902379c723880b90124e3ee160e00000000000000000000000039fd73b03a01c6230b5e0d946e1960d79db44fd80000000000000000000000003eebc158f254838e2f6275b892e6a0621e3ea321000000000000000000000000000000000000000000000000000000000098968000000000000000000000000000000000000000000000000000000000680bc9e000000000000000000000000000000000000000000000000000000000680be8863a30a084fe9ef623cf95ea778067b98b69accf602b8f240f55073339f4c2f2b2000000000000000000000000000000000000000000000000000000000000001bd6960c1cc4c28482a3c96ea35e5c0cfe84f4e466f734de02023b15101c9735a04830dbfeeccd565705c1e8b92b3dd038d720130f5a3101bf43160be49e0f1651c080a0e59d32312a920b6c63ad4c7344bb76d8e7cae2615f79f707649e325abea00a24a07cddec90138bb6790e68e01998fdf77efc9496a91b3b4b42e59fd0e8ad89d0bc",
"transactionHash": "0xc1d5963f87e4a9035eae4e31fe7842a8bc1cd0ebf941d541c0b7ff37b4d1f5df"
}
}
```
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](/cpn/guides/webhooks/setup-webhook-notifications).
# 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
| Blockchain | Transactions V1 | Transactions V2 |
| ---------------- | --------------- | --------------- |
| Ethereum Sepolia | ✅ | ✅ |
| Polygon Amoy | ✅ | ✅ |
| Solana Devnet | ✅ | ✅ |
## Mainnet
| Blockchain | Transactions V1 | Transactions V2 |
| ---------- | --------------- | --------------- |
| Ethereum | ✅ | ✅ |
| Polygon | ✅ | ✅ |
| 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.
* **Ethereum virtual machine (EVM) raw transaction signing:** For Transactions
V1, the provider must be able to sign raw EVM transactions without
broadcasting them to the blockchain. This is not required for Transactions V2.
* **EIP-712 signing:** The provider must support EIP-712 typed data. This is a
requirement for both Transactions V1 and V2. 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. For Transactions
V2, the provider must 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 | Transactions V1 | Transactions V2 |
| ---------- | ----------------------------------------------------------------- | -------------------------------------------- |
| EVM | EIP-712 typed data signing
Raw EVM transaction signing | EIP-712 typed data signing |
| Solana | Solana transaction signing | 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)
# Error Codes
Source: https://developers.circle.com/cpn/references/errors/error-codes
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).
## 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 provide 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. |
## 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. Quotes created with V1 transactions must proceed with V1 transaction endpoints, similarly with quotes created with V2 transactions.
**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. |
## 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. |
# 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 |
| `FIAT_SETTLEMENT_FAILED` | The payment couldn't be completed due to routing or bank settlement issues |
| `COMPLIANCE_CHECK_FAILED` | The payment was rejected due a compliance check failure |
| `CANCELLED` | The payment is canceled by the originator |
| `PAYMENT_EXPIRED` | The payment is expired and not able to provide further action. |
| `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 |
## 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 |
## 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 |
# 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.
# Postman API Suite
Source: https://developers.circle.com/cpn/references/postman
Use Circle's Postman collection to send API requests and try CPN APIs.
Circle's Postman collection provides template requests to help you learn about
the Circle Payments Network APIs. These requests run on Postman, an API platform
for learning, building, and using APIs. The Postman CPN workspace includes a
collection that matches the organization of the
[API References](/api-reference/cpn/cpn-platform/get-payment-configurations-overview).
## Run in Postman
To use the Postman collection, click the **Run in Postman** link below. You can
fork the collection to your workspace, view the collection in the public
workspace, or import the collection into Postman.
* **Fork**: Creates a copy of the collection while maintaining a link to the
parent.
* **View**: Allows you to quickly try out the API without importing anything
into your Postman suite.
* **Import**: Creates a copy of the collection but does not maintain a link to
Circle's copy.
| Collection | Link |
| :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CPN | [Run in Postman](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-4253c25a48d0k) |
**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/)
for details.
You can get an API key for CPN from the
[CPN Console](https://cpn.circle.com/signin). Contact your Circle representative
to get access to CPN.
# 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 `PAYMENT_EXPIRED`. |
| `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.
## 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 webhook notifications](/cpn/guides/webhooks/setup-webhook-notifications) |
| Programmable Wallets | [Create subscription](/api-reference/wallets/common/create-subscription) (`POST /v2/notifications/subscriptions`) | [Webhook notification flows](/wallets/webhook-notification-flows) (for example `transactions.inbound`, `transactions.outbound`) |
| Circle APIs (liquidity) | [Create subscription](/api-reference/circle-mint/general/create-subscription) (`POST /v1/notifications/subscriptions`) | [Notifications data models](/circle-mint/notifications-data-models) |
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.
# 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).
# 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
## 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
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+](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}
npx tsx --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+](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 key pair, 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}
npx tsx --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+](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}
npx tsx --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+](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}
npx tsx --env-file=.env transfer-solana.ts
```
# How-to: Manage Delegates for Smart Contract Wallets
Source: https://developers.circle.com/gateway/howtos/manage-delegates
When using
[Smart Contract Account (SCA) wallets](/wallets/account-types#smart-contract-accounts-sca)
with Gateway, you need to authorize an Externally Owned Account (EOA) to sign
burn intents on behalf of the SCA. This is necessary because Gateway requires
ECDSA signatures for static verification, which SCAs cannot provide directly.
This guide demonstrates how to add and remove delegates for Gateway transfers
from SCA wallets using Circle
[Developer-controlled wallets](/wallets/dev-controlled).
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Created a [Circle Developer Console](https://console.circle.com) account
* Obtained an API key and
[registered your Entity Secret](/wallets/dev-controlled/register-entity-secret)
* Created an SCA wallet and an EOA wallet via Circle Developer-Controlled
Wallets
* The SCA wallet holds your Gateway USDC deposits
* The EOA wallet will sign burn intents on behalf of the SCA
* Deposited USDC into the Gateway Wallet from your SCA 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_SCA_WALLET_ADDRESS}
DELEGATE_WALLET_ADDRESS={YOUR_EOA_DELEGATE_ADDRESS}
```
## Add a Delegate
Follow these steps to authorize an EOA delegate to sign burn intents on behalf
of your SCA 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}
npx tsx --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 SCA wallet when making Gateway transfers.
### Step 3. Repeat for other chains
To add the delegate on additional chains where the SCA holds Gateway deposits,
repeat Steps 1-2 with different `BLOCKCHAIN` and `USDC_ADDRESS` values for your
target chains. 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 SCA wallet. After removing a delegate, note that:
* The delegate can no longer create new burn intents for the SCA wallet
* Burn intents that were already signed by the delegate remain valid and can
still be executed on-chain
* This ensures that burns may be executed safely even in the event of a
revocation
* The Gateway API reflects revocations as soon as they're finalized on-chain
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}
npx tsx --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.
Select a tab below for EVM or Solana-specific instructions.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Prepared an EVM testnet wallet with the private key available
* Added 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 chains 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}
npx tsx --env-file=.env transfer.ts
```
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](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 key pair, 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,
consider setting the `destinationRecipient` to its Associated Token Account
(ATA) not the recipient wallet address. See the [Solana Technical
Guide](/gateway/references/solana#reducedmintattestation) and [Solana Programs
and Interfaces](/gateway/references/solana-programs#gatewaymint) for
high-level Solana guidance and `gatewayMint` account requirements. Use the
onchain IDLs linked from [Solana Programs and
Interfaces](/gateway/references/solana-programs#account-structures) as the
canonical static instruction and account definitions.
```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}
npx tsx --env-file=.env transfer.ts
```
# 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.
## 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+](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 because Gateway verifies payment signatures offchain using
`ecrecover`, which is incompatible with EIP-1271 contract signatures.
## 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="tsx --env-file=.env pay.ts"
# Install runtime dependencies
npm install @circle-fin/x402-batching viem tsx 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+](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="tsx server.ts"
# Install runtime dependencies
npm install @circle-fin/x402-batching @x402/core @x402/evm viem express tsx 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`.