TypeScript examples
TypeScript
The examples use viem and assume a configured wallet client. Error handling is reduced to what illustrates the contract.
Types
type Candidate = {
chainId: number
quoteId: string
expiresAt: number
inputAmount: string
outputAmount: string
recipient?: string
slippageTolerance: number
gasUseEstimate?: string
gasUseEstimateUsd?: string
priceImpactBps?: number
agg?: AggQuote
pcsx?: PcsxOrder
calldata?: { value: `0x${string}`; calldata: `0x${string}`; to: `0x${string}` }
swapFeeBps?: number
swapFee?: string
}
type QuoteResponse = { candidates: Candidate[]; best: Candidate }
type ApiError = { code: string; message: string; upstream?: unknown }Quoting
const BASE = 'https://swap.pancakeswap.com'
const AUTH: Record<string, string> = process.env.PCS_API_KEY
? { 'X-API-KEY': process.env.PCS_API_KEY }
: {}
async function quote(params: Record<string, string>): Promise<QuoteResponse> {
const res = await fetch(`${BASE}/v1/quote?${new URLSearchParams(params)}`, { headers: AUTH })
if (!res.ok) throw await asApiError(res)
return res.json()
}
async function asApiError(res: Response): Promise<Error> {
const text = await res.text()
try {
const body = JSON.parse(text) as ApiError
if (body.code) return Object.assign(new Error(body.message), { code: body.code, status: res.status })
} catch {
// 400 and 422 from the framing layer carry no envelope
}
return Object.assign(new Error(text), { code: `HTTP_${res.status}`, status: res.status })
}
const { best } = await quote({
chainId: '56',
tokenIn: '0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82',
tokenOut: '0x55d398326f99059fF775485246999027B3197955',
amount: '5000000000000000000',
recipient: account.address,
slippageTolerance: '0.005',
sources: 'agg,pcsx',
})Executing an aggregator quote
async function executeAggregator(best: Candidate) {
const res = await fetch(`${BASE}/v1/calldata`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...AUTH },
body: JSON.stringify(best), // the candidate is the body; extra keys are ignored
})
if (!res.ok) throw await asApiError(res)
const tx = await res.json()
return walletClient.sendTransaction({
account,
to: tx.to,
data: tx.calldata,
value: BigInt(tx.value), // hex quantity
})
}The ERC-20 approval for tx.to is the caller's responsibility and is omitted here.
Executing a PancakeSwap X quote
async function executePcsx(best: Candidate) {
const { permitData } = best.pcsx!
const signature = await walletClient.signTypedData({
account,
domain: {
name: permitData.domain.name,
chainId: permitData.domain.chainId,
verifyingContract: permitData.domain.verifyingContract,
},
types: permitData.types,
primaryType: permitData.primaryType,
message: permitData.values, // named `values` on the wire, `message` here
})
const res = await fetch(`${BASE}/v1/submit`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...AUTH },
body: JSON.stringify({ ...best, signature }),
})
if (!res.ok) throw await asApiError(res)
const { hash } = await res.json()
return hash as `0x${string}`
}The Permit2 approval for permitData.domain.verifyingContract is a prerequisite and is omitted here.
Polling to a terminal state
const TERMINAL = new Set(['FILLED', 'EXPIRED', 'CANCELLED'])
async function waitForOrder(chainId: number, hash: string, deadline: number) {
while (Date.now() / 1000 < deadline + 30) {
const res = await fetch(`${BASE}/v1/status/${chainId}/${hash}`, { headers: AUTH })
if (res.ok) {
const order = await res.json()
if (TERMINAL.has(order.status)) return order
} else if (res.status !== 404) {
throw await asApiError(res) // 404 right after submit is a propagation race
}
await new Promise((r) => setTimeout(r, 1500))
}
throw new Error('order did not reach a terminal state before its deadline')
}Branching on the winner
if (best.agg) {
const txHash = await executeAggregator(best)
} else if (best.pcsx) {
const orderHash = await executePcsx(best)
const order = await waitForOrder(best.chainId, orderHash, Number(best.pcsx.orderInfo.deadline))
} else {
throw new Error('candidate names no engine')
}Integration notes
Approvals differ by path. An aggregator swap needs an ERC-20 approval for the router at calldata.to. A PancakeSwap X order needs an approval for Permit2 at pcsx.permitData.domain.verifyingContract. A wallet that has only ever used one path will fail the first time the other engine wins. Check the allowance for whichever path best selects, and note that the Permit2 address is read from the response rather than assumed.
Re-quote rather than retry on 409. QUOTE_EXPIRED means the price you hold is no longer honoured. Retrying the same body produces the same result.
Idempotency of submission rests on the order hash. pcsx.orderHash is known before submission and is returned by it. If a submit response is lost, poll /v1/status with that hash before resubmitting.
Distinguish the two deadlines. expiresAt governs whether the gateway accepts a quote. pcsx.orderInfo.deadline governs whether the order can still settle on chain. Timeout logic keyed on the wrong one will either give up while an order is still fillable or wait long after it died.