bridge-stablecoin
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), andmint(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: trueis 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 thekit.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
.gitignoreentries 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.stepsbefore 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.