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/dist

Create 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
});
OptionTypeMeaning
baseUrlstringThe deployment to talk to. Defaults to https://www.swaps.pro; trailing slashes are trimmed.
partnerstringYour integrator id. Attached to every quote, echoed back, and logged. Truncated to 64 characters server-side.
partnerFeeBpsnumberYour additive fee in basis points. Values above 100 are clamped to 100 (1%); zero, negative and non-finite values become 0.
fetchtypeof fetchOverride 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#

FieldWhenMeaning
provideralwaysThe venue that won the price — e.g. 0x, thorchain, relay, uniswap-rh. SwapsPro aggregates and keeps the better output.
sellAmount / buyAmountalwaysDecimal, human units — the same units you passed in.
ratealwaysbuyAmount / sellAmount, precomputed.
minBuyAmountmost routesThe floor the route was priced against.
txsame-chain EVMA ready-to-send transaction: { chainId, to, data, value, gasLimit }, quantities already 0x-hex.
approvalEVM, ERC-20 sellThe allowance to grant before tx: { chainId, token, spender, amountWei }.
depositAddress + memoTHORChain-style routesSend the sell amount to that address with that memo, from a wallet for the sell chain.
orderCoW routesA signature-settled order to sign and post instead of a transaction to send. Mutually exclusive with tx — see below.
partnerFeewhen partner is set{ requestedBps, collectedBps, collected, recipient?, note } — whether this route actually paid you, and if not, why.
expiresAtalwaysISO 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

The allowance granted is exactly 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);
  }
}
ClasscodeWhat it means
ChainMismatchErrorCHAIN_MISMATCHThe wallet is on err.actual, the quote needs err.expected. Switch and retry.
NotSupportedErrorNOT_SUPPORTEDThe quote has no tx — a memo/deposit route. The message names the deposit address and memo.
SwapsProErrorNO_ROUTENo venue would price the pair (HTTP 404).
SwapsProErrorRATE_LIMITED60 quotes a minute per IP exceeded (HTTP 429).
SwapsProErrorQUOTE_EXPIREDThrown locally when you execute past expiresAt.
SwapsProErrorTX_REVERTEDThe receipt came back with status 0x0.
SwapsProErrorTIMEOUTNo receipt within timeoutMs.
SwapsProErrorNETWORK_ERRORThe 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.

providerCollects your fee?How, or why not
0xYesAdded to SwapsPro's bps inside the 0x swapFee. 0x supports one recipient, so your share is settled to you from the quote log.
cowYesA 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.
lifiYesAdded to the LI.FI integrator fee — one fee wallet, so the same off-chain settlement as 0x.
pioneerNoIts same-chain venues expose no affiliate field, and rewriting a THORChain memo's bps would invalidate the quote's own limit.
uniswap-rhNoA 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.

Even on a collecting route, the money lands in SwapsPro's per-chain recipient — 0x and LI.FI each support a single fee wallet, and CoW's policies pay one address. There is no on-chain claim, no registry contract and no automatic payout.

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

The OG Pro Pass holders pay no SwapsPro fee, but that waiver deliberately does not reach this API: its fee is the price of the API itself, and its callers are integrators rather than the wallet holding the pass. Your 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. executeSwap speaks 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, omit recipient and the API answers BAD_REQUEST.
  • Quotes go stale. Sixty seconds when the venue does not state its own expiry.