Integrate new DEX aggregators, swappers, or bridge protocols (like Bebop, Portals, Jupiter, 0x, 1inch, etc.) into ShapeShift Web. Activates when user wants to add, integrate, or implement support for a new swapper. Guides through research, implementation, and testing following established patterns. (project)
You are an expert at integrating DEX aggregators, swappers, and bridge protocols into ShapeShift Web. This skill guides you through the complete process from API research to production-ready implementation.
Use this skill when the user wants to:
ShapeShift Web is a decentralized crypto exchange aggregator that supports multiple swap providers through a unified interface. Each swapper implements standardized TypeScript interfaces (Swapper and SwapperApi) but has variations based on blockchain type (EVM, UTXO, Solana, Sui, Tron) and swapper model (direct transaction, deposit-to-address, gasless order-based).
Core Architecture:
packages/swapper/src/swappers/Swapper (execution) + SwapperApi (quotes/rates/status)transactionData (a TxBuildData variant) built at quote time. Execution and the public api consume the quote payload as-is — static data is set at quote time, only dynamic data (gas price, solana priority fee, nonce, blockhash) is fetched at execution.helpers.ts, shared getXTradeContext.ts, discriminated getXStepData.ts, thin getTradeQuote/getTradeRate arm wrappers. AcrossSwapper is the spec in code form; the authoritative conventions rubric lives in .claude/skills/swapper-rate-quote-review/SKILL.md — read it alongside this skill.Your Role: Research → Implement → Test → Document, following battle-tested patterns from 18 existing swapper integrations.
BEFORE asking the user for anything, proactively research the swapper online:
Search for official documentation:
Search: "[SwapperName] API documentation"
Search: "[SwapperName] developer docs"
Search: "[SwapperName] swagger api"
Find their website and look for:
Fetch their API docs using WebFetch:
Research chain support:
Search: "[SwapperName] supported chains"
Search: "[SwapperName] which blockchains"
Find existing integrations:
Search: "github [SwapperName] integration example"
Search: "[SwapperName] typescript sdk"
Then, compile what you found and ask the user ONLY for what you couldn't find or need confirmation on.
Use the AskUserQuestion tool to gather missing information with structured prompts.
Based on your Phase 0 research, ask the user for:
API Access (if needed):
Chain Support Confirmation:
Critical API Behaviors (if not clear from docs):
Brand Assets:
Known Issues:
Example Multi-Question Prompt:
AskUserQuestion({
questions: [
{
question: "Do we have an API key for [Swapper]?",
header: "API Key",
multiSelect: false,
options: [
{ label: "Yes, I have it", description: "I'll provide the API key" },
{ label: "No, but we can get one", description: "I'll obtain an API key" },
{ label: "No API key needed", description: "API is public/unauthenticated" }
]
},
{
question: "Which chains should we support initially?",
header: "Chain Support",
multiSelect: true,
options: [
{ label: "Ethereum", description: "Ethereum mainnet" },
{ label: "Polygon", description: "Polygon PoS" },
{ label: "Arbitrum", description: "Arbitrum One" },
{ label: "All supported chains", description: "Enable all chains the API supports" }
]
}
]
})
IMPORTANT: Study existing swappers BEFORE writing any code. This prevents reimplementing solved problems.
Based on API research, determine the swapper type. Every category produces the same canonical
structure — the category only changes what the quote's transactionData variant is and how the
context/step data derive it.
EVM Direct Transaction (Most Common):
ZrxSwapper, PortalsSwapper, BebopSwapper (EVM arm), DebridgeSwapper, AcrossSwappertransactionData: { type: 'evm', chainId, to, data, value, gasLimit } — the
gasLimit is ALWAYS set (provider-supplied, or estimated-and-set by getEvmNetworkFeeCryptoBaseUnit){to, data, value, gas} transaction objectDeposit-to-Address (Cross-Chain/Async):
BobGatewaySwapper (order resolved once up front), ChainflipSwapper
(deposit channel opened quote-side), NearIntentsSwappertransactionData (the transfer we build) PLUS a
swapperMetadata union member holding the tracking id / deposit addressGasless Order-Based:
CowSwapper — transactionData: { type: 'cowswap', chainId, orderToSign },
getUnsignedEvmMessage is a thin reader, executeEvmMessage signs + POSTs the orderSolana:
transactionData: { type: 'solana_instructions', instructions, addressLookupTableAddresses } with the static compute unit limit set at quote time via
withComputeUnitLimit (measured simulation × per-swapper margin); execution fetches only the
dynamic priority fee. Canonical: the solana arms of AcrossSwapper/ButterSwap/RelaySwapper.transactionData: { type: 'solana_serialized_tx', serializedTx } — co-sign as-is, never rebuild. Canonical:
BebopSwapper solana arm.Multi-Chain:
switch (chainNamespace) in step data with BOTH arms
inline per case. Canonical: ButterSwap (evm/utxo/solana/tron), RelaySwapper, NearIntentsSwapper.Chain-Specific (Sui/Tron/Starknet/TON):
transactionData); execution re-derives from
swapperMetadata or provider re-fetch. Canonical: CetusSwapper (sui), SunioSwapper (tron —
the one migrated tron example), AvnuSwapper (starknet), StonfiSwapper (ton). New chain-specific
swappers still get the full context split (Cetus/Stonfi prove it applies without an executable payload).Read the conventions rubric first: .claude/skills/swapper-rate-quote-review/SKILL.md — it is
the authoritative spec for the structure below and its edge cases.
Then read Across — the reference implementation:
packages/swapper/src/swappers/AcrossSwapper/
├── index.ts # Barrel: exports { acrossApi, acrossSwapper } at minimum
├── AcrossSwapper.ts # Swapper interface (shared executors)
├── endpoints.ts # SwapperApi: scoped input casts + shared chain exec utils
├── getTradeQuote/
│ └── getTradeQuote.ts # Quote arm wrapper: assertQuoteAddresses → context → step data → Trade[]
├── getTradeRate/
│ └── getTradeRate.ts # Rate arm wrapper: owns ?? default-address fallbacks → Trade[]
└── utils/
├── types.ts # API types + scoped AcrossTrade{Quote,Rate}Input aliases
├── helpers.ts # PURE helpers: assertValidTrade, address mappers, fee fallbacks
├── acrossService.ts # HTTP client with cache + API key injection
├── fetchAcrossTrade.ts # API wrappers
├── getAcrossTradeContext.ts # Shared core: fetch + derivations, ZERO quoteOrRate checks
└── getAcrossStepData.ts # Discriminated rate/quote step data (StepDataArgs, overloaded)
Then read 1-2 swappers of your category (see canonical examples above).
Critical things to note while reading:
StepDataArgs<Base, RateExtra, QuoteExtra> generic and the overloaded step data returnsmakeNetworkFeeEstimationFailedErr / makeTradeStepBuildFailedErr / makeSwapErrorRighttransactionData variant the quote carries, and what (if anything) goes in swapperMetadataimport { Err, Ok } from '@sniptt/monads'
import { makeSwapErrorRight } from '../../../utils'
// ALWAYS return Result<T, SwapErrorRight>, NEVER throw
const result = await someOperation()
if (result.isErr()) {
return Err(makeSwapErrorRight({
message: 'What went wrong',
code: TradeQuoteError.QueryFailed,
details: { context: 'here' }
}))
}
return Ok(result.unwrap())
import { createCache, makeSwapperAxiosServiceMonadic } from '../../../utils'
const maxAge = 5 * 1000 // 5 seconds
const cachedUrls = ['/quote', '/price'] // which endpoints to cache
const serviceBase = createCache(maxAge, cachedUrls, {
timeout: 10000,
headers: {
'Accept': 'application/json',
'x-api-key': config.VITE_XYZ_API_KEY
}
})
export const xyzService = makeSwapperAxiosServiceMonadic(serviceBase)
For chain adapters and swappers that directly interact with RPC endpoints or APIs:
import PQueue from 'p-queue'
// In constructor or module scope:
private requestQueue: PQueue = new PQueue({
intervalCap: 1, // 1 request per interval
interval: 50, // 50ms between requests
concurrency: 1, // 1 concurrent request at a time
})
// Wrap all external API/RPC calls:
const quote = await this.requestQueue.add(() =>
swapperService.get('/quote', { params })
)
// For provider calls in chain adapters:
const balance = await this.requestQueue.add(() =>
this.provider.getBalance(address)
)
When to use: Any swapper or chain adapter making direct RPC/API calls (especially public endpoints) Example implementations: MonadChainAdapter, PlasmaChainAdapter
import { getInputOutputRate } from '../../../utils'
const rate = getInputOutputRate({
sellAmountCryptoBaseUnit,
buyAmountCryptoBaseUnit,
sellAsset,
buyAsset
})
Follow this EXACT order to avoid rework:
mkdir -p packages/swapper/src/swappers/[SwapperName]Swapper/{getTradeQuote,getTradeRate,utils}
Canonical structure (mirror Across exactly):
[SwapperName]Swapper/
├── index.ts # Barrel: { [swapperName]Api, [swapperName]Swapper } at minimum
├── [SwapperName]Swapper.ts # Swapper interface (shared executors)
├── endpoints.ts # SwapperApi wiring
├── types.ts # Scoped input aliases + metadata type (or utils/types.ts)
├── getTradeQuote/
│ └── getTradeQuote.ts # Quote arm wrapper
├── getTradeRate/
│ └── getTradeRate.ts # Rate arm wrapper
└── utils/
├── constants.ts # Supported chains, native marker, defaults
├── helpers.ts # PURE helpers only (flat file, not helpers/helpers.ts)
├── [swapperName]Service.ts # HTTP client with cache + API key injection
├── fetch[SwapperName]Trade.ts # API wrappers
├── get[SwapperName]TradeContext.ts # Shared core
└── get[SwapperName]StepData.ts # Discriminated rate/quote step data
2a. types.ts - API TypeScript Types
Define types EXACTLY matching the API response (log actual API responses to verify!):
import type { Address, Hex } from 'viem'
// Request types
export type [Swapper]QuoteRequest = {
sellToken: Address
buyToken: Address
sellAmount: string
slippage: number // NOTE: document what format! (percentage, decimal, basis points)
takerAddress: Address
receiverAddress?: Address
chainId: number
}
// Response types (match API exactly!)
export type [Swapper]QuoteResponse = {
// Copy structure from actual API response
buyAmount: string
sellAmount: string
transaction: {
to: Address
data: Hex
value: Hex
gas?: Hex
}
// ... rest of response
}
// Constants
export const [SWAPPER]_SUPPORTED_CHAIN_IDS: Record<number, string> = {
1: 'ethereum',
137: 'polygon',
42161: 'arbitrum',
// ...
}
2b. utils/constants.ts - Configuration
import type { AssetId, ChainId } from '@shapeshiftoss/caip'
import { ethChainId, polygonChainId, arbitrumChainId } from '@shapeshiftoss/caip'
import type { Address } from 'viem'
export const SUPPORTED_CHAIN_IDS = [
ethChainId,
polygonChainId,
arbitrumChainId,
] as const
export type [Swapper]SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]
// Native token marker (if API uses one)
export const NATIVE_TOKEN_MARKER = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' as Address
// Dummy address for rates (when no wallet connected)
export const DUMMY_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' as Address
// Default slippage if none provided
export const DEFAULT_SLIPPAGE_PERCENTAGE = '0.5' // 0.5%
2c. utils/helpers.ts - Pure Helper Functions (incl. assertValidTrade)
import { fromAssetId, type AssetId } from '@shapeshiftoss/caip'
import { isToken } from '@shapeshiftoss/utils'
import { getAddress, type Address } from 'viem'
import { NATIVE_TOKEN_MARKER, SUPPORTED_CHAIN_IDS } from '../constants'
// Check if chain is supported
export const isSupportedChainId = (chainId: string): boolean => {
return SUPPORTED_CHAIN_IDS.includes(chainId as any)
}
// Convert assetId to token address (with native token handling)
export const assetIdToToken = (assetId: AssetId): Address => {
if (!isToken(assetId)) {
return NATIVE_TOKEN_MARKER // Native token (ETH, MATIC, etc.)
}
const { assetReference } = fromAssetId(assetId)
return getAddress(assetReference) // Checksum ERC20 address
}
// Convert ShapeShift chainId to API chain identifier
export const chainIdToChainRef = (chainId: string): string => {
switch (chainId) {
case ethChainId:
return 'ethereum' // or '1' or 'mainnet' depending on API
case polygonChainId:
return 'polygon'
// ...
default:
throw new Error(`Unsupported chainId: ${chainId}`)
}
}
// Calculate rate from amounts
import { getInputOutputRate } from '../../../../utils'
export { getInputOutputRate } // Re-export for use in quote/rate files
2d. utils/[swapperName]Service.ts - HTTP Service
import { createCache, makeSwapperAxiosServiceMonadic } from '../../../utils'
import type { SwapperConfig } from '../../../types'
// Cache for 5 seconds (adjust based on API)
const maxAge = 5 * 1000
// Which endpoints to cache (usually /quote and /price)
const cachedUrls = ['/quote', '/price']
export const [swapperName]ServiceFactory = (config: SwapperConfig) => {
const axiosConfig = {
timeout: 10000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...(config.VITE_[SWAPPER]_API_KEY && {
'x-api-key': config.VITE_[SWAPPER]_API_KEY
})
}
}
const serviceBase = createCache(maxAge, cachedUrls, axiosConfig)
return makeSwapperAxiosServiceMonadic(serviceBase)
}
export type [Swapper]Service = ReturnType<typeof [swapperName]ServiceFactory>
2e. utils/fetchFrom[SwapperName].ts - API Wrappers
import { type AssetId } from '@shapeshiftoss/caip'
import { bn } from '@shapeshiftoss/utils'
import { Err, Ok, type Result } from '@sniptt/monads'
import { getAddress, type Address } from 'viem'
import { makeSwapErrorRight } from '../../../utils'
import { TradeQuoteError, type SwapErrorRight } from '../../../types'
import type { [Swapper]Service } from './[swapperName]Service'
import type { [Swapper]QuoteRequest, [Swapper]QuoteResponse } from '../types'
import { assetIdToToken, chainIdToChainRef } from './helpers'
// Base URL for API
const BASE_URL = 'https://api.[swapper].com'
export type FetchQuoteParams = {
sellAssetId: AssetId
buyAssetId: AssetId
sellAmountCryptoBaseUnit: string
chainId: string
takerAddress: string
receiverAddress: string
slippageTolerancePercentageDecimal: string
affiliateBps: string
}
export const fetchQuote = async (
params: FetchQuoteParams,
service: [Swapper]Service
): Promise<Result<[Swapper]QuoteResponse, SwapErrorRight>> => {
try {
const {
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
chainId,
takerAddress,
receiverAddress,
slippageTolerancePercentageDecimal,
affiliateBps
} = params
// Convert to API format
const sellToken = assetIdToToken(sellAssetId)
const buyToken = assetIdToToken(buyAssetId)
const chainRef = chainIdToChainRef(chainId)
// CRITICAL: Convert slippage to API format
// ShapeShift format: 0.005 = 0.5%
// Check API docs for their format!
const slippagePercentage = bn(slippageTolerancePercentageDecimal)
.times(100) // If API expects 0.5 for 0.5%
.toNumber()
// Checksum addresses (CRITICAL for many APIs)
const checksummedTakerAddress = getAddress(takerAddress)
const checksummedReceiverAddress = getAddress(receiverAddress)
const requestBody: [Swapper]QuoteRequest = {
sellToken,
buyToken,
sellAmount: sellAmountCryptoBaseUnit,
slippage: slippagePercentage,
takerAddress: checksummedTakerAddress,
receiverAddress: checksummedReceiverAddress,
chainId: chainRef,
// Add affiliate if supported
...(affiliateBps !== '0' && { affiliateBps })
}
const maybeResponse = await service.post<[Swapper]QuoteResponse>(
`${BASE_URL}/quote`,
requestBody
)
if (maybeResponse.isErr()) {
return Err(maybeResponse.unwrapErr())
}
const { data: response } = maybeResponse.unwrap()
// Validate response has required fields
if (!response.buyAmount || !response.transaction) {
return Err(
makeSwapErrorRight({
message: 'Invalid response from API',
code: TradeQuoteError.InvalidResponse,
details: { response }
})
)
}
return Ok(response)
} catch (error) {
return Err(
makeSwapErrorRight({
message: 'Failed to fetch quote',
code: TradeQuoteError.QueryFailed,
cause: error
})
)
}
}
// For rates (no wallet needed)
export type FetchPriceParams = Omit<FetchQuoteParams, 'takerAddress' | 'receiverAddress'> & {
receiveAddress: string | undefined
}
export const fetchPrice = async (
params: FetchPriceParams,
service: [Swapper]Service
): Promise<Result<[Swapper]QuoteResponse, SwapErrorRight>> => {
// Use dummy address if no wallet connected
const address = params.receiveAddress
? getAddress(params.receiveAddress)
: DUMMY_ADDRESS
// IMPORTANT: Use same affiliate for both quote and rate to avoid delta!
return fetchQuote(
{
...params,
takerAddress: address,
receiverAddress: address
},
service
)
}
2f. utils/get[SwapperName]TradeContext.ts - Shared Core
The context holds everything BOTH arms share: the provider fetch (when both arms hit the same
endpoint - Across/Debridge model) or just the assembly (when arms fetch differently - Zrx/Portals
model), error mapping, derived amounts, protocolFees, and the step data args. It contains ZERO
quoteOrRate checks and takes already-resolved addresses as params.
type [Swapper]TradeContext = {
tradeCommon: TradeCommon // id, rate, affiliateBps, slippage, swapperName...
stepCommon: Omit<TradeStepCommon, 'feeData'> // amounts, assets, allowanceContract, source...
protocolFees: QuoteFeeData['protocolFees']
stepDataArgs: Omit<Get[Swapper]StepDataArgs, 'type' | 'input'> // also omit arm-divergent extras
}
Rules:
allowanceContract is '' when there is no approval target, never undefinedswapperMetadata (if any) is set here or in the quote wrapper - see Step 3Result - provider errors map to TradeQuoteError codes (QueryFailed, NoRouteFound,
SellAmountBelowMinimum...), never throw2g. utils/get[SwapperName]StepData.ts - Discriminated Step Data
The heart of the rate/quote split. Uses the shared StepDataArgs<Base, RateExtra, QuoteExtra>
generic from types.ts: Base carries deps + sellAsset + everything derived in the context;
the Rate/Quote generics carry arm-specific extras derived in the wrappers (e.g. chainflip's quote
depositAddress). Declare TWO overloads over one implementation so callers get precise per-arm
types:
type [Swapper]RateStepData = { networkFeeCryptoBaseUnit: string }
type [Swapper]QuoteStepData = { transactionData: TxBuildData; networkFeeCryptoBaseUnit: string }
export function get[Swapper]StepData(
args: Extract<Get[Swapper]StepDataArgs, { type: 'rate' }>,
): Promise<Result<[Swapper]RateStepData, SwapErrorRight>>
export function get[Swapper]StepData(
args: Extract<Get[Swapper]StepDataArgs, { type: 'quote' }>,
): Promise<Result<[Swapper]QuoteStepData, SwapErrorRight>>
export async function get[Swapper]StepData(
args: Get[Swapp
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add shapeshift/swapper-integration下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer