Skip to main content

Overview

Portal integrates with Circle’s Cross-Chain Transfer Protocol (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).

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.
  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).
  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.
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 Testnets 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). 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: 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. 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).
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. 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:

Transfer statuses

COMPLETED, FAILED and STALLED are final. A FAILED transfer has a failureReason: 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: