Docs · SDK
Your UI, our routing.
@swapspro/sdk is a headless TypeScript client for SwapsPro. It quotes cross-chain and same-chain swaps over the public HTTP API and executes EVM quotes through any EIP-1193 wallet you hand it. Zero runtime dependencies, tree-shakeable ESM, fully typed — and it never touches keys or funds.
Install#
Published
@swapspro/sdk is on npm at 0.1.1. Building it from packages/sdk still works and produces the same thing — the HTTP API it wraps is live, public and needs no key either way.install
npm i @swapspro/sdk
# Or build it from the repo — the package is the folder, nothing is generated
# that is not committed:
# git clone https://github.com/coinmastersguild/swapspro.git
# cd swapspro && npm install && npm run sdk:build # tsc -> packages/sdk/distCreate a client#
client.ts
import { SwapsPro } from "@swapspro/sdk";
const swaps = new SwapsPro({
partner: "my-app", // your integrator id, attached to every quote
partnerFeeBps: 25, // optional additive fee, capped at 100 (1%)
// baseUrl: "https://www.swaps.pro", // default
// fetch: myFetch, // for tests or exotic runtimes
});| Option | Type | Meaning |
|---|---|---|
| baseUrl | string | The deployment to talk to. Defaults to https://www.swaps.pro; trailing slashes are trimmed. |
| partner | string | Your integrator id. Attached to every quote, echoed back, and logged. Truncated to 64 characters server-side. |
| partnerFeeBps | number | Your additive fee in basis points. Values above 100 are clamped to 100 (1%); zero, negative and non-finite values become 0. |
| fetch | typeof fetch | Override the fetch implementation — for tests, or a runtime without a global one. |
chains()#
Every chain SwapsPro can price, derived from the app's own chain config. The id is the symbol you pass to the other methods; EVM chains also carry a numeric chainId.
chains()
const chains = await swaps.chains();
// [
// { id: "ETH", name: "Ethereum", nativeSymbol: "ETH", caip2: "eip155:1", type: "evm", chainId: 1 },
// { id: "BTC", name: "Bitcoin", nativeSymbol: "BTC", caip2: "bip122:0000...", type: "utxo" },
// { id: "BASE", name: "Base", nativeSymbol: "ETH", caip2: "eip155:8453", type: "evm", chainId: 8453 },
// { id: "SOL", name: "Solana", nativeSymbol: "SOL", caip2: "solana:5eyk...", type: "svm", sellSupported: false },
// ...
// ]type is one of evm, utxo, cosmos, svm or hive. See the API page for the full, real response body.
sellSupported is false when a chain can only be a destination. Solana is the one such chain today: SwapsPro bridges into it from any EVM chain, but has no Solana signer, so there is no transaction it could hand you to sell from it. Quoting sellChain: "SOL" returns UNSUPPORTED_PAIR rather than a route you could not execute.
tokens()#
The curated token list for a chain: the chain's native assets first, then the blue chips SwapsPro ships in its own selector. It is not an exhaustive index — you can quote any ERC-20 by passing its contract address, whether or not it appears here.
tokens()
const tokens = await swaps.tokens(8453); // numeric id
const same = await swaps.tokens("BASE"); // or the chain symbol
// [
// { symbol: "ETH", name: "Base", caip: "eip155:8453/slip44:60", chain: "BASE", isToken: false, decimals: 18 },
// { symbol: "USDC", name: "USD Coin", caip: "eip155:8453/erc20:0x8335...", chain: "BASE", isToken: true, contract: "0x8335...", decimals: 6 },
// { symbol: "WETH", name: "Wrapped Ether", caip: "eip155:8453/erc20:0x4200...", chain: "BASE", isToken: true, contract: "0x4200...", decimals: 18 },
// ]decimals is present only when SwapsPro knows it statically. When it is missing, the quote endpoint resolves the value on-chain for you, so you rarely need to.
quote()#
A firm quote, priced with the same routing the SwapsPro app uses. Your client's partner and partnerFeeBps are attached automatically.
quote()
const quote = await swaps.quote({
sellChain: 8453, // numeric EVM chain id, or a symbol like "BTC"
sellToken: "ETH", // symbol, asset CAIP, or a bare 0x contract address
buyChain: 8453,
buyToken: "USDC",
amount: "0.1", // decimal, human units — never base units
address: account, // sender on the sell chain
// recipient: "0x…", // required when sellChain !== buyChain
// slippage: 1, // percent, 0 < s <= 50
});
console.log(quote.provider, quote.buyAmount, quote.rate);What comes back#
| Field | When | Meaning |
|---|---|---|
| provider | always | The venue that won the price — e.g. 0x, thorchain, relay, uniswap-rh. SwapsPro aggregates and keeps the better output. |
| sellAmount / buyAmount | always | Decimal, human units — the same units you passed in. |
| rate | always | buyAmount / sellAmount, precomputed. |
| minBuyAmount | most routes | The floor the route was priced against. |
| tx | same-chain EVM | A ready-to-send transaction: { chainId, to, data, value, gasLimit }, quantities already 0x-hex. |
| approval | EVM, ERC-20 sell | The allowance to grant before tx: { chainId, token, spender, amountWei }. |
| depositAddress + memo | THORChain-style routes | Send the sell amount to that address with that memo, from a wallet for the sell chain. |
| order | CoW routes | A signature-settled order to sign and post instead of a transaction to send. Mutually exclusive with tx — see below. |
| partnerFee | when partner is set | { requestedBps, collectedBps, collected, recipient?, note } — whether this route actually paid you, and if not, why. |
| expiresAt | always | ISO timestamp. Re-quote after it; executeSwap refuses an expired quote. |
Client-side validation
quote() throws SwapsProError("BAD_REQUEST") before making a request when amount is not a positive number or address is missing, so a typo costs you a round trip rather than a rate-limit slot.executeSwap()#
Hands an EVM quote to a wallet. In order, it: refuses a quote with no tx; refuses a quote whose expiresAt has passed; reads eth_chainId and refuses a mismatch; resolves the signer address (eth_accounts, falling back to eth_requestAccounts); grants the ERC-20 allowance if the quote carries one and the current allowance is short, waiting for that approval to mine; then sends the swap and returns its hash.
executeSwap()
// 1. Sign it. executeSwap checks the wallet's chain, grants the ERC-20
// allowance if the quote needs one (waiting for that approval to mine),
// then sends the swap transaction.
const txHash = await swaps.executeSwap(quote, window.ethereum);
// 2. Wait for it. Poll through the same signer, or through a plain RPC URL.
const receipt = await swaps.waitForReceipt(txHash, { signer: window.ethereum });
console.log("mined in block", parseInt(receipt.blockNumber, 16));Approvals are exact, never unlimited
approval.amountWei — the amount this swap needs. The SDK never asks a user to approve an unbounded amount.Not every quote has a tx. A CoW route settles by signature and a THORChain-style route settles by deposit, so executeSwap throws NotSupportedError for both — with a message that names the steps to take rather than just declining. Branch on quote.order before calling it.
waitForReceipt()#
Polls eth_getTransactionReceipt until a receipt appears. Pass a signer (an EIP-1193 provider) or an rpcUrl — one of the two is required. pollMs defaults to 4000 and timeoutMs to 300000 (five minutes). A receipt whose status is 0x0 throws TX_REVERTED rather than being returned as a success.
Errors#
Every failure is a SwapsProError carrying a machine-readable code and, when it came from the API, the HTTP status. Two subclasses exist because they are the two you will actually branch on.
errors.ts
import { SwapsPro, ChainMismatchError, NotSupportedError, SwapsProError } from "@swapspro/sdk";
try {
const txHash = await swaps.executeSwap(quote, window.ethereum);
} catch (e) {
if (e instanceof ChainMismatchError) {
// The wallet is on e.actual, the quote needs e.expected.
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${e.expected.toString(16)}` }],
});
} else if (e instanceof NotSupportedError) {
// Memo route: send quote.sellAmount to quote.depositAddress with quote.memo,
// using a wallet for the sell chain. There is no EVM tx to sign.
} else if (e instanceof SwapsProError) {
console.error(e.code, e.status, e.message);
}
}| Class | code | What it means |
|---|---|---|
| ChainMismatchError | CHAIN_MISMATCH | The wallet is on err.actual, the quote needs err.expected. Switch and retry. |
| NotSupportedError | NOT_SUPPORTED | The quote has no tx — a memo/deposit route. The message names the deposit address and memo. |
| SwapsProError | NO_ROUTE | No venue would price the pair (HTTP 404). |
| SwapsProError | RATE_LIMITED | 60 quotes a minute per IP exceeded (HTTP 429). |
| SwapsProError | QUOTE_EXPIRED | Thrown locally when you execute past expiresAt. |
| SwapsProError | TX_REVERTED | The receipt came back with status 0x0. |
| SwapsProError | TIMEOUT | No receipt within timeoutMs. |
| SwapsProError | NETWORK_ERROR | The request never reached the API. |
The full list of server-side codes lives on the API page — the SDK passes them through unchanged.
Order routes#
When CoW wins the price race the quote carries order instead of tx. There is nothing to broadcast: you register the fee metadata document, sign the EIP-712 payload with the same address that asked for the quote, and POST the order. The trade is gasless and asynchronous — solvers fill it in a batch auction, usually inside a minute, and it can expire unfilled. quote.approval still applies; its spender is CoW's vault relayer.
order route
import { SwapsPro, NotSupportedError } from "@swapspro/sdk";
const quote = await swaps.quote({ /* … */ });
if (quote.order) {
// A signature-settled route (CoW). Gasless, asynchronous, and it can expire
// unfilled. Three steps, plus the one-time vault-relayer approval.
const { order } = quote;
await fetch(order.appDataUrl, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ fullAppData: order.appData.fullAppData }),
});
const signature = await eth.request({
method: "eth_signTypedData_v4",
params: [account, JSON.stringify(order.typedData)],
});
await fetch(order.postUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...order.body, signature, signingScheme: order.signingScheme }),
});
} else {
// A transaction route. executeSwap throws NotSupportedError on the ones it
// cannot send, and its message names the steps above.
const txHash = await swaps.executeSwap(quote, eth);
}order.note carries the timing trade-off in plain language, so you can show it to a user rather than writing your own.
A site rebuilding its own swap UI#
This is the shape of a real integration: a community site that wants its own swap page in its own design, priced by SwapsPro. React is shown because it is familiar — nothing in the SDK needs it.
SwapButton.tsx
"use client";
import { useState } from "react";
import { SwapsPro, ChainMismatchError } from "@swapspro/sdk";
// One client for the whole app. The partner id rides on every quote.
const swaps = new SwapsPro({ partner: "skatehive", partnerFeeBps: 25 });
export function SwapButton() {
const [amount, setAmount] = useState("0.01");
const [preview, setPreview] = useState("");
const [status, setStatus] = useState("");
// Quote as the user types. Your inputs, your layout, your design system —
// the SDK only prices the trade and hands back a transaction.
async function onQuote(next: string) {
setAmount(next);
const eth = window.ethereum!;
const [account] = (await eth.request({ method: "eth_accounts" })) as string[];
if (!account || !(Number(next) > 0)) return setPreview("");
const quote = await swaps.quote({
sellChain: 8453, sellToken: "ETH",
buyChain: 8453, buyToken: "USDC",
amount: next, address: account,
});
setPreview(`${quote.buyAmount} USDC via ${quote.provider}`);
}
async function onSwap() {
const eth = window.ethereum!;
const [account] = (await eth.request({ method: "eth_requestAccounts" })) as string[];
const quote = await swaps.quote({
sellChain: 8453, sellToken: "ETH",
buyChain: 8453, buyToken: "USDC",
amount, address: account,
});
try {
setStatus("Confirm in your wallet…");
const txHash = await swaps.executeSwap(quote, eth);
setStatus("Waiting for the network…");
const receipt = await swaps.waitForReceipt(txHash, { signer: eth });
setStatus(`Swapped in block ${parseInt(receipt.blockNumber, 16)}`);
} catch (e) {
if (e instanceof ChainMismatchError) {
await eth.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${e.expected.toString(16)}` }],
});
return onSwap(); // retry on the right chain
}
throw e;
}
}
return (
<div>
<input value={amount} onChange={(e) => onQuote(e.target.value)} />
<p>{preview}</p>
<button onClick={onSwap}>Swap</button>
<p>{status}</p>
</div>
);
}Three things worth copying from it: one client for the whole app so the partner id cannot be forgotten on a code path; quoting on input and executing on click, because a quote is cheap and disposable; and catching ChainMismatchError to switch networks rather than telling the user to.
No SDK — plain fetch#
The SDK is a thin typed wrapper over three CORS-open GET endpoints. If you are not in a JavaScript runtime, skip it entirely.
curl
# Python, Go, Rust, Unity, a shell script — anything that speaks HTTP.
BASE=https://www.swaps.pro/api/sdk/v1
curl -s "$BASE/quote?sellChain=8453&sellToken=ETH&buyChain=8453&buyToken=USDC\
&amount=0.1&address=0xYourAddress&partner=my-app&partnerFeeBps=25"
# Same-chain EVM: the response carries tx (and approval for ERC-20 sells).
# Pass tx straight to eth_sendTransaction — the quantities are already 0x-hex.And the same thing in the browser without the package:
fetch
const BASE = "https://www.swaps.pro/api/sdk/v1";
const params = new URLSearchParams({
sellChain: "8453", sellToken: "ETH",
buyChain: "8453", buyToken: "USDC",
amount: "0.1", address: account,
partner: "my-app",
});
const res = await fetch(`${BASE}/quote?${params}`);
const quote = await res.json();
if (!res.ok) throw new Error(`${quote.code}: ${quote.error}`);
// Approve first when selling an ERC-20 (quote.approval), then send the swap.
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [{
from: account,
to: quote.tx.to,
data: quote.tx.data,
value: quote.tx.value,
gas: quote.tx.gasLimit,
}],
});Partner attribution, honestly#
partner is a free-form integrator id. partnerFeeBps is a fee charged on top of the standard SwapsPro fee — it can never replace or redirect it, because the SwapsPro fee is stamped server-side into the quote before you ever see it. The cap is 100 basis points, enforced with Math.min(Math.floor(n), 100): asking for more silently becomes 100, and a negative or non-numeric value becomes 0.
Whether you earn depends on which venue wins#
Three of the five venues the router can pick will carry a second fee and two cannot. Rather than echo your number back regardless, the response reports what actually happened.
| provider | Collects your fee? | How, or why not |
|---|---|---|
0x | Yes | Added to SwapsPro's bps inside the 0x swapFee. 0x supports one recipient, so your share is settled to you from the quote log. |
cow | Yes | A genuine second volume policy in the order's appData. CoW caps the TOTAL partner fee at 100 bps and SwapsPro's 30 bps is paid first, so a bigger request is clamped — and reported as clamped rather than silently reduced. |
lifi | Yes | Added to the LI.FI integrator fee — one fee wallet, so the same off-chain settlement as 0x. |
pioneer | No | Its same-chain venues expose no affiliate field, and rewriting a THORChain memo's bps would invalidate the quote's own limit. |
uniswap-rh | No | A raw Uniswap v3 router call on Robinhood Chain — there is no fee hook in the call. |
Read it per quote, do not assume it#
partnerFee
const quote = await swaps.quote({ /* … */ });
// Did this route actually pay you? Ask the quote, do not assume.
console.log(quote.provider); // "lifi"
console.log(quote.partnerFee?.collected); // true
console.log(quote.partnerFee?.note);
// "Collected on-chain: 25 bps added to SwapsPro's 30 bps in the LI.FI
// integrator fee. LI.FI supports a single fee wallet, so your share is
// settled to you from the quote log."
// On a route that cannot carry one, partnerFeeBps is ABSENT and the note says why:
// "NOT collected: this route was priced by Pioneer, whose venues expose no
// affiliate field we can add a partner fee to (and rewriting a THORChain
// memo's bps would invalidate the quote's own limit). Quote for a pair CoW,
// LI.FI or 0x can route to earn on it."partnerFeeBps is present only when a fee is genuinely collected. If it is absent, nothing was taken for you on that route — and partnerFee.note says why in a sentence you can log or surface.
Attribution works today. Settlement does not exist yet.
Being straight with you about the rest: there is no partner registration process. The id is a string you choose; nothing issues it, nothing validates it, and no revenue-share agreement exists to settle against. Your quotes are attributed in our logs from the moment you send one, so the record is being kept — but until an agreement exists there is nothing to pay out from it. Build on
partner for your own analytics, and do not build a business model on partnerFeeBps yet.A Pro Pass never waives your fee
partnerFeeBps is unaffected either way. See Free and Pro.Limits#
- Hive is out of scope. A Hive swap needs a Hive wallet, so any pair touching Hive returns
UNSUPPORTED_PAIR. Use the app for those. - Execution covers EVM only.
executeSwapspeaks EIP-1193. Bitcoin, Litecoin, Dogecoin and Cosmos routes come back as a deposit address and a memo for you to pay with a wallet for that chain. - Cross-chain needs a recipient. When
sellChain !== buyChain, omitrecipientand the API answersBAD_REQUEST. - Quotes go stale. Sixty seconds when the venue does not state its own expiry.
