SDK for seamless cross-chain stablecoin bridging
npm install @circle-fin/bridge-kit



A strongly-typed SDK for seamless cross-chain stablecoin bridging
_Making cross-chain stablecoin (USDC, and soon more tokens) transfers as simple as a single function call_
- Bridge Kit
- Table of Contents
- Overview
- Why Bridge Kit?
- Architecture Flow
- Installation
- Adapters
- Quick Start
- π Easiest Setup: Single Adapter, Multiple Chains
- π― Send to Different Address
- π Production Setup: Custom RPC Providers
- π Browser/Wallet Provider Support
- π§ Advanced Setup: Full Control
- π Cost Estimation
- Configuration
- Bridge Configuration Types
- 1. AdapterContext - Your Transfer Endpoint
- 2. BridgeDestination - Where Funds Go
- 3. BridgeConfig - Transfer Settings
- Complete Example with All Options
- Bridge Speed Configuration
- Custom Fees
- How Custom Fees Work
- 1000 USDC Transfer Example
- Kit-Level Fee Policies
- Error Handling
- Retrying Failed Transfers
- Examples
- Basic Bridge Operation
- EVM to Non-EVM Bridge (EVM β Solana)
- Third-Party Recipient
- Event Monitoring
- API Reference
- Core Methods
- Bridge Parameters
- Development
- Building
- Testing
- Local Development
- Contributing
- Quick Contribution Steps
- Community \& Support
- License
The Stablecoin Kit ecosystem is Circleβs open-source effort to streamline stablecoin development with SDKs that are easy to use correctly and hard to misuse. Kits are cross-framework (viem, ethers, @solana/web3) and integrate cleanly into any stack. Theyβre opinionated with sensible defaults, but offer escape hatches for full control. A pluggable architecture makes implementation flexible, and all kits are interoperable, so they can be composed to suit a wide range of use cases.
The Bridge Kit enables cross-chain stablecoin transfers via a type-safe, developer-friendly interface with robust runtime validation. The Kit can have any bridging provider plugged in, by implementing your own BridgingProvider, but comes by default with full CCTPv2 support
- π Bridge-first design: All abstractions revolve around _source_ β _destination_ chain pairs
- β‘ Zero-config defaults: Built-in reliable RPC endpoints - start building right away
- π§ Bring your own infrastructure: Seamlessly integrate with your existing setup when needed
- π Production-ready security: Leverages Circle's CCTPv2 with deterministic quotes and finality tracking
- π Developer experience: Complete TypeScript support, comprehensive validation, and instant connectivity
- π Cross-chain bridging: The Bridge Kit supports 37 chains with 666 total bridge routes through Circle's CCTPv2
- Mainnet (18 chains): Arbitrum, Avalanche, Base, Codex, Ethereum, HyperEVM, Ink, Linea, Monad, OP Mainnet, Plume, Polygon PoS, Sei, Solana, Sonic, Unichain, World Chain, XDC
- Testnet (19 chains): Arc Testnet, Arbitrum Sepolia, Avalanche Fuji, Base Sepolia, Codex Testnet, Ethereum Sepolia, HyperEVM Testnet, Ink Testnet, Linea Sepolia, Monad Testnet, OP Sepolia, Plume Testnet, Polygon PoS Amoy, Sei Testnet, Solana Devnet, Sonic Testnet, Unichain Sepolia, World Chain Sepolia, XDC Apothem
- π― Flexible adapters: Supporting EVM (Viem, Ethers) and Solana (@solana/web3)
- βοΈ Configurable bridge speeds: FAST/SLOW options with fee optimization
- π‘ Real-time event monitoring: Track progress throughout the transfer lifecycle
- π‘οΈ Robust error handling: Graceful partial success recovery
- βοΈ Pre-flight validation: Verify transfers with cost estimation before execution
The Bridge Kit follows a three-layer architecture designed for flexibility and type safety:
``text`
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β Bridge Kit ββββββ Provider ββββββ Adapter β
β (Orchestrator) β β (Protocol) β β (Blockchain) β
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
1. Adapter: Handles blockchain-specific operations (wallets, transactions, gas) and enables you to use whatever framework you're comfortable with (viem, ethers, @solana/web3, and more coming soon)
2. Provider: Implements bridging protocols (currently CCTPv2)
3. BridgeKit: Orchestrates adapters and providers with auto-routing and validation
This separation ensures that each component has a single responsibility while maintaining seamless integration across the entire cross-chain bridging lifecycle.
`bash`
npm install @circle-fin/bridge-kitor
yarn add @circle-fin/bridge-kit
Choose the appropriate adapter for your target chains:
`bashFor EVM chains (Ethereum, Base, Arbitrum, etc.)
npm install @circle-fin/adapter-viem-v2 viemor
yarn add @circle-fin/adapter-viem-v2 viem
Quick Start
$3
Best for: Getting started quickly, simple transfers using one wallet across chains
The factory methods make it incredibly easy to get started with built-in reliable RPC endpoints. No need to research providers or configure endpoints - just start building! Create one adapter and use it across different chains!
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'// Initialize the kit
const kit = new BridgeKit()
// Create ONE adapter that works across all chains!
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
})
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '10.50',
})
`β¨ Key Feature: All supported chains include reliable default RPC endpoints.
$3
Best for: Sending funds to someone else's wallet, custodial services
Use
BridgeDestinationWithAddress when the recipient is different from your adapter's address:`typescript
// Send to a different address on the destination chain
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: {
adapter,
chain: 'Base',
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
},
amount: '10.50',
})// Or use a different adapter for the destination chain
const baseAdapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
})
const resultWithDifferentAdapter = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: {
adapter: baseAdapter,
chain: 'Base',
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
},
amount: '10.50',
})
`$3
Best for: Production applications, better reliability, custom configuration
Bridging involves two chains (source and destination), and both require properly configured RPC endpoints. Use dynamic RPC mapping by chain ID to support multiple chains in a single adapter:
`typescript
import 'dotenv/config'
import { BridgeKit } from '@circle-fin/bridge-kit'
import { Ethereum, Base } from '@circle-fin/bridge-kit/chains'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { createPublicClient, http, fallback } from 'viem'// Define RPCs mapped by chain ID
const RPC_BY_CHAIN_ID: Record = {
// The array allows providing multiple RPC URLs for fallback, e.g.,
//
[ "https://primary-rpc-url.com/...", "https://secondary-rpc-url.com/..." ]
[Ethereum.chainId]: [
https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY},
],
[Base.chainId]: [
https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY},
],
}const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as
0x${string},
getPublicClient: ({ chain }) => {
const rpcUrls = RPC_BY_CHAIN_ID[chain.id]
if (!rpcUrls) {
throw new Error(No RPC configured for chainId=${chain.id})
}
return createPublicClient({
chain,
transport: fallback(
rpcUrls.map((url) =>
http(url, {
timeout: 10_000,
retryCount: 3,
}),
),
),
})
},
})const kit = new BridgeKit()
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '10.50',
})
`Best practices:
- Use paid RPC providers (Alchemy, Infura, QuickNode) for improved reliability
- Implement
fallback() transport for automatic failover between endpoints
- Configure timeout and retry options to handle network variability$3
Best for: Browser applications, wallet integrations, user-controlled transactions
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromProvider } from '@circle-fin/adapter-viem-v2'const kit = new BridgeKit()
// Create adapters from browser wallet providers
const adapter = await createViemAdapterFromProvider({
provider: window.ethereum,
})
// Execute bridge operation
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '10.50',
})
`$3
Best for: Advanced users, custom client configuration, specific RPC requirements
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { Ethereum, Base } from '@circle-fin/bridge-kit/chains'
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'const account = privateKeyToAccount(process.env.PRIVATE_KEY as string)
// Chain-specific RPC URLs mapped by chain ID
const rpcUrls: Record = {
[Ethereum.chainId]: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
[Base.chainId]: 'https://base-mainnet.g.alchemy.com/v2/YOUR_KEY',
}
// Create one multi-chain adapter with chain-specific RPC configuration
const adapter = new ViemAdapter(
{
getPublicClient: ({ chain }) =>
createPublicClient({
chain,
transport: http(rpcUrls[chain.id]),
}),
getWalletClient: ({ chain }) =>
createWalletClient({
account,
chain,
transport: http(rpcUrls[chain.id]),
}),
},
{
addressContext: 'user-controlled',
supportedChains: [Ethereum, Base], // Support multiple chains!
},
)
const kit = new BridgeKit()
// Execute bridge operation using the same adapter for both chains
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '10.50',
})
`$3
Best for: Showing users fees upfront, budget planning
`typescript
// Get cost estimate before bridging
const estimate = await kit.estimate({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '10.50',
})console.log('Estimated fees:', estimate.fees)
console.log('Estimated gas:', estimate.gasFees)
`Configuration
$3
The Bridge Kit supports different configuration patterns to match your use case:
#### 1. AdapterContext - Your Transfer Endpoint
`typescript
// Create chain-agnostic adapter
const adapter = createViemAdapterFromPrivateKey({...})// Always specify chain explicitly for clarity
const adapterContext = { adapter, chain: 'Ethereum' }
`#### 2. BridgeDestination - Where Funds Go
`typescript
// Same as AdapterContext (adapter receives the funds)
const destination = { adapter, chain: 'Base' }// Or with explicit recipient address
const destination = {
adapter,
chain: 'Base',
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
}
`#### 3. BridgeConfig - Transfer Settings
`typescript
// FAST: Optimized for speed with higher fees
// SLOW: Optimized for lower fees with longer processing time
const config = { transferSpeed: 'FAST' }
`$3
`typescript
// Fast transfer (higher fees, faster completion)
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '100.0',
config: { transferSpeed: 'FAST' },
})// Slow transfer (lower fees, slower completion)
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '100.0',
config: { transferSpeed: 'SLOW' },
})
`Custom Fees
Bridge Kit allows you to charge custom developer fees on cross-chain USDC transfers. Understanding how these fees interact with wallet debits and CCTPv2 protocol fees is crucial for correct implementation.
$3
Custom fees are added on top of the transfer amount, not taken out of it. The wallet signs for
transfer amount + custom fee, so the user must have enough balance for both values. The entire transfer amount continues through CCTPv2 unchanged, while the custom fee is split on the source chain:- 10% of the custom fee is automatically routed to Circle.
- 90% is sent to your
recipientAddress.
- Important: Circle only takes the 10% share when a custom fee is actually charged. If you omit a custom fee, Circle does not collect anything beyond the protocol fee.After the custom fee is collected, the transfer amount (e.g., 1,000 USDC) proceeds through CCTPv2, where the protocol applies its own fee (1β14 bps in FAST mode, 0% in STANDARD).
Fee Flow (1,000 USDC transfer + 10 USDC custom fee):
`
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User signs: 1,000 USDC transfer + 10 USDC custom fee = 1,010 β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Custom fee distribution (source chain) β
β - 1 USDC (10%) β Circle β
β - 9 USDC (90%) β Your fee recipient β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CCTPv2 processes full transfer amount (1,000 USDC) β
β - Protocol fee example (FAST 1 bps): 0.1 USDC β
β - Destination receives: 999.9 USDC β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
`$3
Scenario: Transfer 1,000 USDC from Ethereum to Base with a 10 USDC custom fee.
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as
0x${string},
})await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '1000', // Transfer amount forwarded to CCTPv2
config: {
customFee: {
value: '10', // Additional debit charged on top of the transfer amount
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
},
},
})
`What happens on-chain:
| Stage | Amount | Description |
| ----------------------------- | ---------- | ------------------------------------------------------ |
| Transfer amount | 1,000 USDC | Forwarded to CCTPv2 without reduction |
| Custom fee debit | +10 USDC | Wallet signs for 1,010 USDC total |
| Custom fee β Circle (10%) | 1 USDC | Automatically routed to Circle |
| Custom fee β You (90%) | 9 USDC | Sent to
0x742d35Cc...bEb0 (your fee recipient) |
| CCTPv2 fee (FAST 1 bps)\* | -0.1 USDC | Protocol fee taken from the 1,000 USDC transfer amount |
| Destination receives | 999.9 USDC | Amount minted on Base after protocol fee |\* \_CCTPv2 FAST transfers charge 1β14 bps depending on the route; STANDARD transfers charge 0 bps.
$3
For dynamic fee calculation across all transfers, use kit-level policies:
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as
0x${string},
})kit.setCustomFeePolicy({
computeFee: (params) => {
const amount = parseFloat(params.amount)
const feePercentage = 0.01 // 1%
const calculatedFee = amount * feePercentage
// Return human-readable fee (e.g., '10' for 10 USDC)
return calculatedFee.toFixed(6)
},
resolveFeeRecipientAddress: (feePayoutChain) => {
// Return appropriate address for source chain
return feePayoutChain.type === 'solana'
? 'SolanaAddressBase58...'
: '0xEvmAddress...'
},
})
// All subsequent bridges will use this policy
await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '1000', // Custom fee calculated automatically
})
`> Note: The
calculateFee function is deprecated. Use computeFee instead, which receives human-readable amounts (e.g., '100' for 100 USDC) rather than smallest-unit amounts.Error Handling
The kit uses a thoughtful error handling approach:
- Hard errors (thrown): Validation, configuration, and authentication errors
- Soft errors (returned): Recoverable issues like insufficient balance or network errors
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
})
const params = {
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '100.0',
}
const result = await kit.bridge(params)
if (result.state === 'success') {
console.log('Bridge successful!')
} else {
// Handle partial completion with recovery information
console.log(
'Successful steps:',
result.steps.filter((s) => s.state === 'success'),
)
}
`Retrying Failed Transfers
Use
BridgeKit.retry to resume failed or incomplete bridge operations when the failure is actionable (e.g., transient RPC issues, dropped transactions, or a failed step in a multi-step flow). The kit delegates retry to the original provider (CCTPv2 supports actionable retries) and continues from the appropriate step.$3
`typescript
retry<
TFromAdapterCapabilities extends AdapterCapabilities,
TToAdapterCapabilities extends AdapterCapabilities
>(
result: BridgeResult,
context: RetryContext
): Promise
`$3
`typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as
0x${string},
})const result = await kit.bridge({
from: { adapter, chain: 'Ethereum_Sepolia' },
to: { adapter, chain: 'Base_Sepolia' },
amount: '1',
})
if (result.state === 'error') {
try {
const retryResult = await kit.retry(result, { from: adapter, to: adapter })
console.log('Retry state:', retryResult.state)
} catch (error) {
console.error('Retry failed:', error)
}
}
`$3
- Retry: transient network/RPC errors, gas repricing/dropped txs, step failure with progress recorded.
- Manual: insufficient funds, incorrect recipient, unsupported route, or errors indicating non-actionable state.
$3
- Only actionable failures can be retried; some failures require user action first.
- Source and destination chains must still be supported by the provider (CCTPv2).
- Provide valid adapters for both
from and to contexts.$3
- Use exponential backoff on transient failures; avoid rapid replay.
- Reprice gas sensibly on congested networks.
- Persist
result.steps and tx hashes to aid observability and support.$3
- "Retry not supported for this result, requires user action": fix balances/addresses/attestation issues and try again.
- "Provider not found": ensure the same provider (e.g., CCTPv2) is present in
BridgeKit configuration.> See a runnable example at
examples/basic-usdc-transfer/src/retry.ts (script: yarn start:retry).API Reference
$3
-
kit.bridge(params) - Execute cross-chain bridge operation
- kit.estimate(params) - Get cost estimates before bridging
- kit.retry(result, context) - Resume actionable failed/partial transfers
- kit.supportsRoute(source, destination, token) - Check route support
- kit.on(event, handler) - Listen to bridge events
- kit.off(event, handler) - Removes the listener from bridge events$3
`typescript
interface BridgeParams {
from: AdapterContext // Source wallet and chain
to: BridgeDestination // Destination wallet/address and chain
amount: string // Amount to transfer (e.g., '10.50')
token?: 'USDC' // Optional, defaults to 'USDC'
config?: BridgeConfig // Optional bridge configuration (e.g., transfer speed). If omitted, defaults will be used
}// AdapterContext: Your blockchain connection
type AdapterContext = {
adapter: Adapter
chain: ChainIdentifier
address?: string // Required for developer-controlled adapters; forbidden for user-controlled
}
// BridgeDestination: Where funds go
type BridgeDestination =
| AdapterContext
| {
adapter: Adapter // Adapter for the destination chain
chain: ChainIdentifier // Chain identifier
recipientAddress: string // Custom recipient address
}
`Development
$3
`bash
From the root of the monorepo
nx build @circle-fin/bridge-kit
`$3
`bash
From the root of the monorepo
nx test @circle-fin/bridge-kit
`$3
`bash
Install dependencies
yarn installBuild all packages
yarn buildBuild the bridge-kit specifically
nx build @circle-fin/bridge-kitRun tests
nx test @circle-fin/bridge-kit
``- π¬ Discord: Join our community
This project is licensed under the Apache 2.0 License. Contact support for details.
---