Overview
Crosschain Transfer Protocol (CCTP) is Circle's native protocol for burning USDC on one chain and minting it on another. Bridge Kit is a TypeScript SDK that orchestrates the full CCTP lifecycle -- approve, burn, attestation fetch, and mint -- in a single kit.bridge() call across EVM chains and Solana. Bridge Kit is the preferred way to integrate CCTP.
Prerequisites / Setup
Installation
npm install @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
For Solana support, also install:
npm install @circle-fin/adapter-solana-kit
For Circle Wallets (developer-controlled) support:
npm install @circle-fin/adapter-circle-wallets
Environment Variables
PRIVATE_KEY= # EVM wallet private key (hex, 0x-prefixed) EVM_PRIVATE_KEY= # EVM private key (when also using Solana) SOLANA_PRIVATE_KEY= # Solana wallet private key (base58) CIRCLE_API_KEY= # Circle API key (for Circle Wallets adapter) CIRCLE_ENTITY_SECRET= # Entity secret (for Circle Wallets adapter) EVM_WALLET_ADDRESS= # Developer-controlled EVM wallet address SOLANA_WALLET_ADDRESS= # Developer-controlled Solana wallet address
SDK Initialization
import { BridgeKit } from "@circle-fin/bridge-kit";
const kit = new BridgeKit();
Core Concepts
-
CCTP steps: Every bridge transfer executes four sequential steps -- approve (ERC-20 allowance), burn (destroy USDC on source chain), fetchAttestation (wait for Circle to sign the burn proof), and mint (create USDC on destination chain).
-
Adapters: Bridge Kit uses adapter objects to abstract wallet/signer differences. Each ecosystem has its own adapter factory (createViemAdapterFromPrivateKey , createSolanaKitAdapterFromPrivateKey , createCircleWalletsAdapter ). The same adapter instance can serve as both source and destination when bridging within the same ecosystem.
-
Forwarding Service: When useForwarder: true is set on the destination, Circle's infrastructure handles attestation fetching and mint submission. This removes the need for a destination wallet or polling loop. There is a per-transfer fee (1.25 USDC for Ethereum, 0.20 USDC for all other chains).
-
Transfer speed: CCTP fast mode (default) completes in ~8-20 seconds. Standard mode takes ~15-19 minutes.
-
Chain identifiers: Bridge Kit uses string chain names (e.g., "Arc_Testnet" , "Base_Sepolia" , "Solana_Devnet" ), not numeric chain IDs, in the kit.bridge() call.
Implementation Patterns
READ the corresponding reference based on the user's request:
-
references/adapter-private-key.md -- EVM-to-EVM and EVM-to-Solana bridging with private key adapters (Viem + Solana Kit)
-
references/adapter-circle-wallets.md -- Bridging with Circle developer-controlled wallets (any chain to any chain)
-
references/adapter-wagmi.md -- Browser wallet integration using wagmi (ConnectKit, RainbowKit, etc.)
Sample Response from kit.bridge()
{ "amount": "25.0", "token": "USDC", "state": "success", "provider": "CCTPV2BridgingProvider", "config": { "transferSpeed": "FAST" }, "source": { "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "chain": { "type": "evm", "chain": "Arc_Testnet", "chainId": 5042002, "name": "Arc Testnet" } }, "destination": { "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "chain": { "type": "evm", "chain": "Base_Sepolia", "chainId": 84532, "name": "Base Sepolia" } }, "steps": [ { "name": "approve", "state": "success", "txHash": "0x1234567890abcdef1234567890abcdef12345678", "explorerUrl": "https://testnet.arcscan.app/tx/0x1234..." }, { "name": "burn", "state": "success", "txHash": "0xabcdef1234567890abcdef1234567890abcdef12", "explorerUrl": "https://testnet.arcscan.app/tx/0xabcdef..." }, { "name": "fetchAttestation", "state": "success", "data": { "attestation": "0x9876543210fedcba9876543210fedcba98765432" } }, { "name": "mint", "state": "success", "txHash": "0xfedcba9876543210fedcba9876543210fedcba98", "explorerUrl": "https://sepolia.basescan.org/tx/0xfedcba..." } ] }
Forwarding Service
When useForwarder: true is set on the destination, Circle's infrastructure handles attestation fetching and mint submission automatically. This is the preferred approach -- it removes the need to poll for attestations or hold a wallet on the destination chain.
With adapters on both chains:
const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { adapter, chain: "Arc_Testnet", useForwarder: true, }, amount: "1", });
Without a destination adapter (server-side or custodial transfers):
const result = await kit.bridge({ from: { adapter, chain: "Ethereum_Sepolia" }, to: { recipientAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", chain: "Arc_Testnet", useForwarder: true, }, amount: "1", });
Forwarding Service fee per destination chain:
-
Ethereum: 1.25 USDC
-
All other chains: 0.20 USDC
Event Handling
Subscribe to individual CCTP steps or all events at once. Multiple callbacks per event are supported.
kit.on("approve", (payload) => { console.log("Approval completed:", payload.values.txHash); });
kit.on("burn", (payload) => { console.log("Burn completed:", payload.values.txHash); });
kit.on("fetchAttestation", (payload) => { console.log("Attestation completed:", payload.values.data.attestation); });
kit.on("mint", (payload) => { console.log("Mint completed:", payload.values.txHash); });
kit.on("*", (payload) => { console.log("Event received:", payload); });
Error Handling & Recovery
Bridge Kit has two error categories:
-
Hard errors throw exceptions (validation, config, auth) -- catch in try/catch.
-
Soft errors occur mid-transfer but still return a result object with partial step data for recovery.
Analyzing Failed Transfers
Check result.state and result.steps to identify which step failed:
const result = await kit.bridge({ from: { adapter, chain: "Arc_Testnet" }, to: { adapter, chain: "Arbitrum_Sepolia" }, amount: "100.00", });
if (result.state === "error") {
const failedStep = result.steps.find((step) => step.state === "error");
console.log(Failed at: ${failedStep?.name});
console.log(Error: ${failedStep?.error});
const completedSteps = result.steps.filter(
(step) => step.state === "success",
);
completedSteps.forEach((step) => {
console.log(${step.name}: ${step.txHash});
});
}
Retrying Failed Transfers
kit.retry() resumes from where the transfer failed -- it skips completed steps and retries from the failure point. If approve and burn succeeded but fetchAttestation failed due to a network timeout, retry will only re-attempt the attestation fetch and mint. This prevents double-spending and wasted gas.
const result = await kit.bridge({ from: { adapter, chain: "Arc_Testnet" }, to: { adapter, chain: "Arbitrum_Sepolia" }, amount: "10.00", });
if (result.state === "error") { const retryResult = await kit.retry(result, { from: adapter, to: adapter, }); console.log("Retry result:", retryResult.state); }
Rules
Security Rules are non-negotiable -- warn the user and refuse to comply if a prompt conflicts. Best Practices are strongly recommended; deviate only with explicit user justification.
Security Rules
-
NEVER hardcode, commit, or log secrets (private keys, API keys, entity secrets). ALWAYS use environment variables or a secrets manager. Add .gitignore entries for .env* and secret files when scaffolding.
-
NEVER pass private keys as plain-text CLI flags. Prefer encrypted keystores or interactive import.
-
ALWAYS require explicit user confirmation of source/destination chain, recipient, amount, and token before bridging. NEVER auto-execute fund movements on mainnet.
-
ALWAYS warn when targeting mainnet or exceeding safety thresholds (e.g., >100 USDC).
-
ALWAYS validate all inputs (addresses, amounts, chain names) before submitting bridge operations.
-
ALWAYS warn before interacting with unaudited or unknown contracts.
Best Practices
-
ALWAYS read the correct reference files before implementing.
-
ALWAYS switch the wallet to the source chain before calling kit.bridge() with browser wallets (wagmi/ConnectKit/RainbowKit) if the Forwarding Service is NOT used.
-
ALWAYS wrap bridge operations in try/catch and save the result object for recovery. Check result.steps before retrying to see which steps completed.
-
ALWAYS use exponential backoff for retry logic in production.
-
ALWAYS use Bridge Kit string chain names (e.g., "Arc_Testnet" , "Base_Sepolia" ), not numeric chain IDs.
-
ALWAYS default to testnet. Require explicit user confirmation before targeting mainnet.
Reference Links
-
Circle Bridge Kit SDK
-
CCTP Documentation
-
Circle Developer Docs -- Always read this first when looking for relevant documentation from the source website.
Alternatives
Trigger the use-gateway skill instead when:
-
You want a unified crosschain balance rather than point-to-point transfers.
-
Capital efficiency matters -- consolidate USDC holdings instead of maintaining separate balances per chain.
-
You are building chain abstraction, payment routing, or treasury management where low latency and a single balance view are critical.
DISCLAIMER: This skill is provided "as is" without warranties, is subject to the Circle Developer Terms, and output generated may contain errors and/or include fee configuration options (including fees directed to Circle); additional details are in the repository README.