@circle-fin/provider-stablecoin-service-swap
v1.0.1
Published
<div align="center">
Downloads
237
Keywords
Readme
Stablecoin Service Swap Provider
Circle's Stablecoin Service swap provider
Managed single-chain token swaps backed by Circle's Stablecoin Service API.
Table of Contents
- Stablecoin Service Swap Provider
Overview
The StablecoinServiceSwapProvider enables token swaps on individual blockchain networks through Circle's managed Stablecoin Service. This provider integrates with DEX aggregators to find optimal swap routes while maintaining the security and reliability standards of the App Kits ecosystem.
While primarily designed to power the Swap Kit, this provider can also be used directly in applications that need fine-grained control over the swap process or want to integrate without the full App Kits framework.
Installation
npm install @circle-fin/provider-stablecoin-service-swap
# or
yarn add @circle-fin/provider-stablecoin-service-swapNote: This provider is included by default with the Swap Kit. Import this provider directly if you need custom swap integration.
Quick Start
Option 1: With Swap Kit (Recommended)
import { createSwapKitContext, swap, Ethereum } from '@circle-fin/swap-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
// Create adapter
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
// Provider included by default
const context = createSwapKitContext()
const result = await swap(context, {
from: { adapter, chain: Ethereum },
tokenIn: 'USDC',
tokenOut: 'USDT',
amountIn: '100.50',
})Option 2: Direct Provider Usage
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.PRIVATE_KEY)
const provider = new StablecoinServiceSwapProvider()
// Create adapter
const adapter = new ViemAdapter({
publicClient: createPublicClient({ chain: mainnet, transport: http() }),
walletClient: createWalletClient({
account,
chain: mainnet,
transport: http(),
}),
})
// Check route support
const isSupported = provider.supportsRoute(
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
'0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
Ethereum,
)
// Get swap estimate
const estimate = await provider.estimate({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
amountIn: '100.50',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: { slippageBps: 300 },
})
// Execute swap
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
amountIn: '100.50',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: { slippageBps: 300 },
})Features
- ✅ Single-chain swaps - Swap tokens on the same blockchain
- ✅ Broad token support - Stablecoins, wrapped tokens, and native tokens
- ✅ Comprehensive validation - Route validation and parameter checking
- ✅ Type safety - Full TypeScript support with detailed error handling
- ✅ Enhanced error debugging - Automatic transaction hash and explorer URL inclusion in post-submission errors
- ✅ Permit support - Gas-efficient EIP-2612 permit signatures when available
- ✅ Custom fees - Configurable developer fees for monetization
- ✅ Slippage protection - Stop-limit enforcement to prevent adverse swaps
Supported Chains
EVM Chains: Base, Ethereum, Polygon PoS, and more
Non-EVM Chains: Solana
Note: Testnet chains are not supported for swap operations.
Supported Tokens
Each swap operation supports trading between the following token categories:
Stablecoins (6 decimals):
- USDC - USD Coin
- EURC - Euro Coin
- USDT - Tether USD
- PYUSD - PayPal USD
Stablecoins (18 decimals):
- DAI - MakerDAO stablecoin
- USDE - Ethena USD (synthetic dollar)
Wrapped Tokens:
- WBTC - Wrapped Bitcoin (8 decimals)
- WETH - Wrapped Ethereum (18 decimals)
- WSOL - Wrapped Solana (9 decimals)
- WAVAX - Wrapped Avalanche (18 decimals)
- WPOL - Wrapped Polygon (18 decimals)
Native Token:
- NATIVE - Chain's native gas token (ETH on Ethereum, SOL on Solana, etc.)
Token availability varies by chain. Use supportsRoute() to verify support before executing swaps.
EIP-2612 Gasless Approvals
The provider automatically uses EIP-2612 permit signatures for supported tokens, enabling single-transaction swaps without requiring separate approval transactions.
How It Works
For EIP-2612 Supported Tokens (USDC):
- User signs a permit (off-chain, gasless)
- Swap executes with permit included
- Total: 1 transaction
For Non-EIP-2612 Tokens (USDT, DAI, etc.):
- Approval transaction executes and confirms
- Swap executes
- Total: 2 transactions
The provider automatically detects token support and handles the appropriate flow.
Automatic Approval for All ERC-20 Tokens
The provider now supports automatic approval for all ERC-20 tokens, not just USDC. When a token doesn't support EIP-2612 permits, the provider intelligently selects the appropriate approval method:
USDC Approval Strategy:
- Uses
increaseAllowance()function (OpenZeppelin ERC20 extension) - Safer approach that avoids race conditions
- Relative increase instead of absolute value
- Recommended by Circle for USDC
Other ERC-20 Tokens (USDT, DAI, etc.):
- Uses standard
approve()function (ERC-20 specification) - Industry standard used by Uniswap, 1inch, and all major DEXs
- Theoretical race condition is rare in practice
- Atomic swap execution provides additional safety
Race Condition Note:
The standard ERC-20 approve() function has a known theoretical race condition where a malicious spender could front-run an approval change. However:
- This is extremely rare in practice
- It's the accepted industry standard (used by all major DEXs)
- The adapter contract is trusted and swaps are atomic
- USDC uses the safer
increaseAllowance()to avoid this entirely
Supported EIP-2612 Tokens
| Token | Chains | | -------- | -------------------------------------------------------------------------------------- | | USDC | Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche, World Chain, Sepolia (testnet) |
Code Examples
Swap with EIP-2612 (Single Transaction)
// Swap USDC → USDT (USDC supports EIP-2612)
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: 'USDC', // Supports EIP-2612
tokenOutAddress: 'USDT',
amountIn: '100.00',
to: '0xRecipient',
})
// Flow: User signs permit → Swap executes (1 tx)
console.log('Completed in 1 transaction:', result.txHash)Swap USDT with Automatic Approval (Two Transactions)
// Swap USDT → USDC (USDT doesn't support EIP-2612)
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
tokenOutAddress: 'USDC',
amountIn: '100.00',
to: '0xRecipient',
})
// Flow: Approval tx (uses token.approve) → Wait → Swap tx (2 txs)
console.log('Completed in 2 transactions:', result.txHash)Swap DAI with Automatic Approval (Two Transactions)
// Swap DAI → USDC (DAI doesn't support EIP-2612 on some chains)
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
tokenOutAddress: 'USDC',
amountIn: '1000.00',
to: '0xRecipient',
})
// Flow: Approval tx (uses token.approve) → Wait → Swap tx (2 txs)
console.log('Completed in 2 transactions:', result.txHash)Generic ERC-20 Token Swap
// Swap any ERC-20 token (automatic approval detection)
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xYourTokenAddress',
tokenOutAddress: 'USDC',
amountIn: '100.00',
to: '0xRecipient',
})
// Provider automatically:
// 1. Detects if token supports EIP-2612
// 2. Uses permit (1 tx) or approve (2 tx) accordingly
// 3. Selects increaseAllowance (USDC) or approve (other tokens)
console.log('Swap completed:', result.txHash)User Experience Comparison
| Scenario | Transactions | User Actions | Gas Cost | | ------------------------- | ------------ | ------------------------------- | -------- | | EIP-2612 Token (USDC) | 1 | Sign permit + Confirm swap | Lower ⚡ | | Non-EIP-2612 Token | 2 | Confirm approval + Confirm swap | Higher |
Usage Examples
Basic Swap Operation
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
// Create adapter
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
// Instantiate provider
const provider = new StablecoinServiceSwapProvider()
// Execute swap: USDC → USDT on Ethereum
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: {
slippageBps: 300, // 3% max slippage
allowanceStrategy: 'permit', // Use EIP-2612 permit
},
})
console.log('Transaction hash:', result.txHash)
console.log('Fees:', result.fees)Getting a Swap Estimate
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
const provider = new StablecoinServiceSwapProvider()
// Get estimate before swapping
const estimate = await provider.estimate({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: { slippageBps: 300 },
})
console.log('Stop limit:', estimate.stopLimit) // { token: 'USDT', amount: '99.5' }
console.log('Estimated output:', estimate.estimatedOutput) // { token: 'USDT', amount: '99.5' }
console.log('Fee breakdown:', estimate.fees)Route Validation
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { Ethereum, Base, Solana } from '@core/chains'
const provider = new StablecoinServiceSwapProvider()
// Check if USDC → USDT is supported on Ethereum
const ethereumSupported = provider.supportsRoute(
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
'0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
Ethereum,
)
console.log('Ethereum USDC→USDT:', ethereumSupported) // true
// Check native token swap on Base
const baseNativeSupported = provider.supportsRoute(
'0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
'NATIVE', // Native gas token
Base,
)
console.log('Base USDC→NATIVE:', baseNativeSupported) // true
// Solana support
const solanaSupported = provider.supportsRoute(
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC on Solana
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // USDT on Solana
Solana,
)
console.log('Solana USDC→USDT:', solanaSupported) // trueCustom Fee Configuration
The provider supports two custom fee approaches (mutually exclusive):
Option 1: Percentage-Based (Simple)
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
const provider = new StablecoinServiceSwapProvider()
// Percentage fee per-swap
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: {
customFee: {
percentageBps: 1000, // 10% fee
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
},
},
})Option 2: Absolute Amount (Via SwapKit Callback)
// When using SwapKit with callback, provider receives absolute amount
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
config: {
customFee: {
amount: '5000000', // 5 USDC - calculated by SwapKit callback
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
},
},
})Note: Provider accepts either
percentageBpsORamount(mutually exclusive). Service calculates the fee forpercentageBps, or uses the providedamountdirectly.
Configuration Options
Swap Configuration
| Option | Type | Default | Description |
| ---------------------------- | ----------------------- | ---------- | ------------------------------------------------------------ |
| allowanceStrategy | 'permit' \| 'approve' | 'permit' | Token approval strategy |
| slippageBps | number | 300 | Max slippage in basis points (300 = 3%) |
| stopLimit | string | - | Minimum acceptable output in base units |
| customFee.percentageBps | number | - | Fee percentage (100 = 1%, mutually exclusive with amount) |
| customFee.recipientAddress | string | - | Address that receives the fee (required if customFee is set) |
Custom Fee: Provide either
percentageBps(service calculates) ORamount(absolute amount), not both. |customFee.value|string| - | Per-swap fee override in base units | |customFee.recipientAddress|string| - | Per-swap fee recipient override | |kitKey|string| - | Kit identifier for tracking | |provider|string| - | DEX aggregator identifier |
Error Handling
The provider implements comprehensive error handling using KitError. Errors are categorized into two types:
- Fatal errors - Unrecoverable errors that require configuration changes or user action (e.g., invalid parameters, unsupported routes, insufficient balance)
- Retryable errors - Transient errors that may succeed on retry (e.g., network issues, service temporarily unavailable)
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { isKitError, isFatalError, isRetryableError } from '@core/errors'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
const provider = new StablecoinServiceSwapProvider()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
const params = {
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
}
try {
// Check route first
const isSupported = provider.supportsRoute(
params.tokenInAddress,
params.tokenOutAddress,
Ethereum,
)
if (!isSupported) {
console.error('Route not supported')
return
}
const result = await provider.swap(params)
console.log('Swap completed:', result.txHash)
} catch (error) {
if (isKitError(error)) {
console.error('Error code:', error.code)
console.error('Message:', error.message)
if (isFatalError(error)) {
// Unrecoverable error - fix configuration
console.error('Fatal error, check your parameters')
} else if (isRetryableError(error)) {
// Transient error - retry is possible
console.log('Retrying...')
}
} else {
// Unexpected error
console.error('Unexpected error:', error)
}
}Accessing Transaction Details from Errors
When a swap transaction fails after submission (e.g., reverts on-chain, runs out of gas), the error automatically includes the transaction hash and block explorer URL for easy debugging:
import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
import { isKitError } from '@core/errors'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@core/chains'
const provider = new StablecoinServiceSwapProvider()
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
try {
const result = await provider.swap({
from: { adapter, chain: Ethereum },
tokenInAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenOutAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
amountIn: '100.00',
to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
} catch (error) {
if (isKitError(error)) {
console.error('Swap failed:', error.message)
console.error('Error code:', error.code)
// Access transaction details from error trace
const trace = error.cause?.trace as any
if (trace?.txHash) {
console.log('Transaction hash:', trace.txHash)
// Output: 0x1234567890abcdef...
}
if (trace?.explorerUrl) {
console.log('View on explorer:', trace.explorerUrl)
// Output: https://etherscan.io/tx/0x1234567890abcdef...
}
}
}Note: Transaction details (txHash and explorerUrl) are only included for errors that occur after the transaction has been submitted to the blockchain. Pre-submission errors (validation, insufficient balance, etc.) will not include these fields.
Common Error Scenarios
| Error | Cause | Resolution | Transaction Details |
| -------------------- | --------------------------------- | ----------------------------------------------- | -------------------- |
| Route not supported | Token pair not available on chain | Use supportsRoute() to verify before swapping | ❌ No |
| Validation failed | Invalid parameters | Check parameter format and types | ❌ No |
| Insufficient balance | Not enough tokens | Verify wallet balance before swap | ❌ No |
| Slippage exceeded | Price moved beyond tolerance | Increase slippageBps or retry | ✅ Yes (if on-chain) |
| Transaction reverted | On-chain execution failed | Check explorerUrl in error trace for details | ✅ Yes |
| Out of gas | Insufficient gas for execution | Check explorerUrl in error trace for details | ✅ Yes |
Swap Process
The provider handles the complete swap flow:
- Validation - Validate input parameters and route support
- Approval - Grant token allowance (via permit or approve transaction)
- Execution - Submit swap transaction to the network
- Confirmation - Wait for transaction confirmation
Each step is tracked and can be monitored through the result object.
Service Integration Architecture
Overview
The StablecoinServiceSwapProvider integrates with Circle's Stablecoin Service API to enable managed, secure token swaps. The integration follows a client-server architecture where the provider acts as a client to Circle's hosted swap service.
Integration Flow
The provider handles the following steps:
- Validate params & route - Ensures the swap request is valid
- Build swap request - Constructs the API request payload
- Call Stablecoin Service API - Makes HTTPS request to Circle's service
- Process response - Validates and parses the service response
- Execute blockchain transaction - Submits the transaction via the adapter
API Endpoints
The provider interacts with these Stablecoin Service API endpoints:
Quote Endpoint
- Purpose: Get swap quote with estimated amounts and fees
- Method:
GET /v1/stablecoinKits/quote - Authentication: API key via Authorization header
- Request: Query parameters with token addresses, amount, slippage
- Response: Quote with estimated output, minimum output, and fee context
Swap Execution Endpoint
- Purpose: Execute the actual swap transaction
- Method:
POST /v1/stablecoinKits/swap - Authentication: API key via Authorization header
- Request: Swap parameters including quote details and addresses
- Response: Transaction data with instructions for execution
Authentication
The service integration requires authentication with Circle's Stablecoin Service:
// Authentication is handled internally by the provider
// Configuration can be provided via:
// 1. Environment variables (recommended for production)
process.env.CIRCLE_KIT_KEY = 'KIT_KEY:example-key-id:example-key-secret'
// 2. Provider configuration (for testing/development)
const provider = new StablecoinServiceSwapProvider({
kitKey: 'KIT_KEY:example-key-id:example-key-secret',
customFee: {
amount: '1000000',
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
},
})Request Flow
Swap Request
// Provider handles:
// a) Token approval (via EIP-2612 permit or approve tx)
// b) Swap execution request to service
// c) Transaction submission via adapter
// d) Confirmation waiting
// Returns:
{
txHash: '0xabc123...',
tokenInAddress: '0xA0b...',
tokenOutAddress: '0xdAC...',
amountIn: '100000000',
fees: [{ token: 'USDC', amount: '0.003' }]
}Integration with Swap Kit
This provider is designed specifically for the Swap Kit:
import {
createSwapKitContext,
estimate,
swap,
getSupportedChains,
} from '@circle-fin/swap-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@circle-fin/swap-kit'
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
})
// Provider is included by default
const context = createSwapKitContext()
// Get supported chains from all providers
const chains = getSupportedChains(context)
console.log(
'Supported chains:',
chains.map((c) => c.name),
)
// Execute swap
const result = await swap(context, {
from: { adapter, chain: Ethereum },
tokenIn: 'USDC',
tokenOut: 'USDT',
amountIn: '100.50',
config: { slippageBps: 300 },
})
console.log('Transaction:', result.txHash)API Reference
Core Methods
supportsRoute(tokenInAddress, tokenOutAddress, chain)
Check whether the provider supports a swap route.
- Parameters:
tokenInAddress: string- Input token address or native currency symboltokenOutAddress: string- Output token address or native currency symbolchain: ChainDefinition- The chain where the swap will occur
- Returns:
boolean-trueif the route is supported
estimate(params)
Get a swap estimate without executing the transaction.
- Parameters:
ServiceSwapParams- Swap parameters - Returns:
Promise<EstimateResult>- Estimate including transaction data and fees
swap(params)
Execute a token swap operation.
- Parameters:
ServiceSwapParams- Swap parameters - Returns:
Promise<SwapResult>- Result including transaction hash and swap details
Types
| Type | Description |
| ------------------------------------- | --------------------------------------------------- |
| StablecoinServiceSwapProviderConfig | Provider constructor configuration |
| ServiceSwapParams | Parameters for swap operations |
| ServiceSwapConfig | Optional swap configuration |
| EstimateResult | Swap estimate response |
| SwapResult | Swap execution result |
| SwappingProvider | Interface for swap provider implementations |
| AllowanceStrategy | Token approval strategy ('permit' or 'approve') |
Development
This package is part of the App Kits monorepo.
# Build
nx build @circle-fin/provider-stablecoin-service-swap
# Test
nx test @circle-fin/provider-stablecoin-service-swap
# Lint
nx lint @circle-fin/provider-stablecoin-service-swapContributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
This project is licensed under the Apache 2.0 License. Contact support for details.
Ready for single-chain swapping?
Join Discord • Visit our Help-Desk
Built with ❤️ by Circle
