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

# CCTP

> Move USDC between chains with CCTP. Your users sign only on the source chain.

## Overview

Portal integrates with Circle's [Cross-Chain Transfer Protocol (CCTP)](https://developers.circle.com/cctp) so you can move USDC between EVM chains and Solana through Portal APIs. CCTP burns USDC on the source chain and Circle mints the same amount of native USDC on the destination chain. There are no wrapped tokens or liquidity pools involved.

Portal uses Circle's Forwarding Service, so the user signs only on the source chain. Circle mints on the destination chain and pays the gas there, so the wallet needs no gas on the destination. Circle's protocol and forwarding fees come out of the amount you send. You can also charge your own fee on top, paid in USDC on the source chain (see [Fees](#fees)).

## How it works

1. **Quote**: `POST /quote` returns the fees, the exact amount the recipient receives and a set of preflight checks. Nothing is stored.
2. **Create**: `POST /transfers` quotes again with fresh fees, runs every check and returns what to sign: EVM transactions, a user operation for Account Abstraction clients, or one Solana transaction for a Solana source.
3. **Sign**: the wallet signs and broadcasts on the source chain with the Enclave MPC API.
4. **Submit**: `POST /transfers/{id}/submit` tells Portal the hash (or Solana signature) of the burn.
5. **Track**: Portal follows the burn through Circle's attestation and the mint on the destination chain. Poll `GET /transfers/{id}` for the status.

All endpoints live under `https://api.portalhq.io/api/v3/clients/me/integrations/cctp`.

## Enable the CCTP integration in Portal

CCTP is keyless: there is no Circle account or API key to set up.

1. Open the [Portal Dashboard](https://app.portalhq.io).
2. Navigate to the `Integrations` page.
3. Locate the `CCTP` integration and click `Add`.
4. Optionally, set your own fee under **Your fee** (see [Custodian fee](#custodian-fee)).
5. Click `Enable`.

To change your fee later, click `Configure` on the CCTP card and save.

Until it's enabled, the routes, quote and create endpoints return `400` with the id `CCTP_NOT_ENABLED`. Transfers that were already created can still be submitted and tracked.

> [!NOTE]
>
> Integrations are environment‑scoped. Repeat these steps per Portal environment.

## Supported chains

Any supported chain can be the source or the destination: transfers run between two EVM chains, from any EVM chain to Solana and from Solana to any EVM chain. Both chains must be mainnets or both must be testnets. `GET /routes` returns the live list.

The client needs a wallet on the source chain, and on the destination chain too unless you pass a `recipient`. A Solana source needs a Solana wallet, and sending to the client's own address on Solana needs one as well. Otherwise the `ROUTE_SUPPORTED` check fails.

**Mainnets**

| Chain | Chain ID | Fast | Standard |
| - | - | - | - |
| Ethereum | `eip155:1` | \~20 seconds | \~18 minutes |
| Avalanche | `eip155:43114` | Not offered | \~8 seconds |
| OP Mainnet | `eip155:10` | \~8 seconds | \~18 minutes |
| Arbitrum | `eip155:42161` | \~8 seconds | \~18 minutes |
| Base | `eip155:8453` | \~8 seconds | \~18 minutes |
| Polygon PoS | `eip155:137` | Not offered | \~8 seconds |
| Linea | `eip155:59144` | \~8 seconds | 6 to 32 hours |
| Monad | `eip155:143` | Not offered | \~5 seconds |
| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Not offered | \~25 seconds |

**Testnets**

| Chain | Chain ID | Fast | Standard |
| - | - | - | - |
| Ethereum Sepolia | `eip155:11155111` | \~20 seconds | \~18 minutes |
| Avalanche Fuji | `eip155:43113` | Not offered | \~8 seconds |
| Arbitrum Sepolia | `eip155:421614` | \~8 seconds | \~18 minutes |
| Base Sepolia | `eip155:84532` | \~8 seconds | \~18 minutes |
| Polygon Amoy | `eip155:80002` | Not offered | \~8 seconds |
| Monad Testnet | `eip155:10143` | Not offered | \~5 seconds |
| Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | Not offered | \~25 seconds |

Times are estimates and depend on the **source** chain. Circle only offers Fast from chains where Standard is slow; the others already settle in seconds.

## Speed

Pass `speed` on the quote and create requests:

* `auto` (default): Fast when the source chain offers it, otherwise Standard.
* `fast`: same as `auto`. A source chain without Fast still uses Standard.
* `standard`: always Standard. From Ethereum and the L2s above this takes minutes, and from Linea hours. The response carries a `LONG_FINALITY` warning when Standard is slow.

The `speed` field in the response is the speed Portal actually used. Portal runs transfers from Solana at Standard, so a transfer from Solana always uses Standard (about 25 seconds), even when you ask for `fast`. There's no Fast fee from Solana.

## Fees

Every quote shows the fees and the exact amount the recipient receives. Circle's fees come out of the amount:

* **Protocol fee** (`fees.protocol`): Circle's fee, in basis points of the amount, rounded up to the nearest USDC unit (0.000001). Circle sets the rate per route and speed.
* **Forwarding fee** (`fees.forwarding`): what Circle charges to mint on the destination chain and pay the gas there.
* **Max fee** (`fees.maxFee`): protocol fee plus forwarding fee.

Your fee is charged on top of the amount, in USDC on the source chain:

* **Custodian fee** (`fees.custodian`): your fee, set on the CCTP card in the Portal Dashboard (see [Custodian fee](#custodian-fee)). `0` when you haven't set one.
* **Total debit** (`totalDebit`): `amount + fees.custodian`, the USDC that leaves the wallet.

The recipient receives `amount - maxFee` (`amountOut`). Circle can charge less than `maxFee`, in which case the recipient gets slightly more, but in practice it charges the full `maxFee`. For example, sending `0.1` USDC from Base Sepolia to Avalanche Fuji with a 0.5% custodian fee:

| Field | Value |
| - | - |
| `amount` | `0.1` |
| `fees.protocol` | `0.000013` |
| `fees.forwarding` | `0.054252` |
| `fees.maxFee` | `0.054265` |
| `fees.custodian` | `0.0005` |
| `totalDebit` | `0.1005` |
| `amountOut` | `0.045735` |

Fees change with gas prices on the destination chain. `POST /transfers` always quotes again, so show your user the fees, `totalDebit` and `amountOut` from that response, not from an earlier quote.

The amount must be larger than Circle's fees. `minAmount` in the quote is the smallest amount that delivers anything on that route.

Forwarding into Solana costs more. Circle mints into the recipient wallet's USDC token account, and when that account doesn't exist yet, Circle creates it during the mint and charges for it. On mainnet the forwarding fee is about 0.32 USDC into a new token account and about 0.14 USDC into an existing one, compared with about 0.05 USDC on EVM routes. The quote carries the `RECIPIENT_ACCOUNT_SETUP` warning when Circle will create the account, and `minAmount` reflects the higher fee.

### Custodian fee

You can charge your own fee on each transfer. Set it on the CCTP card in the Portal Dashboard, under **Your fee**:

* **Fee**: a percentage of `amount`, in steps of 0.01%. Portal rounds it down to the nearest USDC unit (0.000001).
* **Flat fee (USDC)**: a fixed USDC amount per transfer.
* **EVM fee address** and **Solana fee address**: where the fee goes.

You can set a percentage, a flat fee or both, and they add up. The fee is paid in USDC on the source chain: to your EVM fee address when the transfer starts on an EVM chain, and to your Solana fee address when it starts on Solana. Both addresses are required when you set a fee.

The fee is charged on top of `amount`, so it doesn't change `amountOut`. The `BALANCE` check makes sure the wallet holds `totalDebit`. On an EVM source, each fee is a separate USDC transfer in `transactions` (`kind: "fee"`), after the burn. For Account Abstraction clients the fee transfers are inside the user operation, and on a Solana source they're inside the burn transaction.

### Exact-out transfers

To have the recipient receive an exact amount, send `amountOut` instead of `amount`. Portal works out how much to burn so that the recipient gets at least `amountOut` after Circle's fees (exactly `amountOut` when Circle charges the full `maxFee`, as it does in practice). Your fee is still charged on top, with any percentage applied to the `amount` Portal burns. Send exactly one of `amount` or `amountOut`.

Both are decimal USDC strings with up to 6 decimals, for example `"0.1"`, not base units.

## Recipients

By default the USDC goes to the client's own Portal address on the destination chain.

You can send it elsewhere with `recipient`: an EVM address for an EVM destination, or a Solana wallet address for a Solana destination. Portal checks the address against the destination chain. When `recipient` isn't one of the client's own addresses, it must be on your environment's destination allowlist, which you manage on the **Destination Allowlist** page of the [Portal Dashboard](https://app.portalhq.io). Otherwise the `RECIPIENT_ALLOWED` check fails and the transfer isn't built. A recipient other than the client's own destination address also adds the `EXTERNAL_RECIPIENT` warning.

On Solana, Circle mints into the recipient wallet's USDC token account, so pass the wallet address, not a token account address. When that token account doesn't exist yet, Circle creates it during the mint, the forwarding fee is higher and the quote carries the `RECIPIENT_ACCOUNT_SETUP` warning (see [Fees](#fees)).

> [!NOTE]
>
> These recipient checks apply to transfers built through the Portal CCTP API.

## Preflight checks

The quote and create endpoints return a `checks` array. Each check has an `id`, an `ok` value and, when it fails, a `message` and a `fix`.

* `ok: true`: the check passed.
* `ok: false`: the check failed and blocks the transfer.
* `ok: null`: the check can't be known yet, or a check it depends on failed. It doesn't block.

`ready` is `true` when no check is `false`. `POST /quote` returns `200` either way. `POST /transfers` returns `400` with the id `CCTP_PREFLIGHT_FAILED` and every failed check, and stores nothing.

| Check | Passes when |
| - | - |
| `ROUTE_SUPPORTED` | Both chains are supported and different, both are mainnets or both are testnets, the client has a wallet on the source chain (and on the destination chain when there's no `recipient`), and Circle currently offers forwarded transfers on the route. For Account Abstraction clients on an EVM source, Account Abstraction must also be configured for the source chain. When it fails, the other checks are `null`. |
| `AMOUNT_ABOVE_MINIMUM` | The amount is larger than Circle's fees, so the recipient receives something. The fix names the minimum. |
| `BALANCE` | The wallet holds at least `totalDebit` USDC on the source chain: `amount` plus your fee. |
| `RECIPIENT_ALLOWED` | The recipient is one of the client's own addresses or is on the destination allowlist. |
| `SOURCE_GAS` | The wallet can pay the source chain gas. For EOA clients on an EVM source, the native balance covers the estimated gas for all the transactions (`null` if the estimate is unavailable). For Account Abstraction clients on an EVM source, the user operation is sponsored or the smart account can pay for it. For a Solana source, the gas sponsor pays, or the wallet holds enough SOL for the transaction fees and rent and still keeps Solana's minimum account balance (about 0.0009 SOL) afterwards. |
| `SPONSORSHIP` | Account Abstraction clients and Solana sources only. The user operation (or Solana transaction) is sponsored, or it isn't and the smart account (or Solana wallet) holds enough native gas to pay for it. |

For Account Abstraction clients on an EVM source, `SOURCE_GAS` and `SPONSORSHIP` are `null` on `POST /quote`, because Portal only builds the user operation on `POST /transfers`. They are also `null` on `POST /transfers` when an earlier check fails. For a Solana source, both checks are known on the quote.

### Solana gas

When your environment has a Solana gas sponsor that's enabled, included in your plan and funded, the sponsor pays the transaction fee for a Solana source, plus the rent (about 0.003 SOL) for the event account that Circle's program creates with each burn. The wallet then needs no SOL. Otherwise the wallet pays both, and must keep Solana's minimum account balance (about 0.0009 SOL) on top. `sourceGas.payer` in the quote says who pays, and `sourceGas.estimatedLamports` how much.

### Warnings

`warnings` never block a transfer:

| Warning | Meaning |
| - | - |
| `LONG_FINALITY` | The transfer uses Standard from a chain where Standard takes minutes or hours. |
| `EXTERNAL_RECIPIENT` | The recipient isn't the client's own address on the destination chain. |
| `RECIPIENT_ACCOUNT_SETUP` | The Solana recipient has no USDC token account yet. Circle creates it during the mint and charges a higher forwarding fee. |

## Transfer statuses

| Status | Meaning |
| - | - |
| `BUILT` | Created. Waiting for you to sign, broadcast and submit it. |
| `SUBMITTED` | Portal has the hash (or Solana signature) and is waiting for the source transaction (or user operation) to be mined and verified. |
| `PENDING_SOURCE` | The burn is mined and verified. Circle hasn't picked it up yet. |
| `ATTESTING` | Circle has seen the burn and is waiting for enough source chain confirmations to attest it. |
| `DELAYED` | Circle delayed the attestation. `delayReason` says why, for example `insufficient_fee`, `amount_above_max` or `insufficient_allowance_available`. |
| `FORWARDING` | Circle attested the burn and is minting on the destination chain. |
| `COMPLETED` | The USDC was minted to the recipient. `destinationTxHash` is set. |
| `FAILED` | The transfer can't complete. See `failureReason` below. |
| `ABANDONED` | Built but not submitted within 1 hour, and no burn for it landed. If its burn did land, Portal submits it for you instead. A late submit is still accepted. |
| `STALLED` | Portal stopped tracking the transfer 24 hours after it was submitted (48 hours from Linea, whose Standard finality can take up to 32 hours) without seeing it complete. Check the explorer links, and contact Portal support with the transfer id if the funds haven't arrived. |

`COMPLETED`, `FAILED` and `STALLED` are final.

A `FAILED` transfer has a `failureReason`:

| Failure reason | Meaning |
| - | - |
| `SOURCE_REVERTED` | The source transaction or user operation reverted. No USDC was burned. |
| `TX_MISMATCH` | The submitted transaction doesn't contain the burn Portal built, for example because the approve or a fee transfer hash was submitted instead of the burn hash. |
| `USER_OP_NOT_FOUND` | The submitted user operation wasn't included on chain within 30 minutes. |
| `SOURCE_TX_NOT_FOUND` | The submitted Solana signature didn't appear on chain within 30 minutes, for example because the transaction expired before it landed. |

Portal tracks transfers through Circle's attestation API. While it's unavailable, transfers wait in their current status until it's back; Circle still forwards the mint, so the USDC still arrives. `STALLED` still applies 24 hours after submission (48 hours from Linea). If a transfer is `STALLED` and the funds haven't arrived, the burn's message and attestation can be fetched from Circle's attestation API with `sourceTxHash` and minted on the destination chain manually; contact Portal support with the transfer id. On Solana, `destinationTxHash` is a base58 signature and `destinationExplorerUrl` links to Solscan.

## Next steps

Implement the flow in your app:

* [Move USDC with CCTP](/apis/enclave-mpc/guide/cctp) using the Enclave MPC API
* [CCTP endpoints](/api-reference/cctp/create-a-cctp-transfer) in the Client API reference


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.