Quotes & Routing

Best execution on BTR is computed off chain. A quote or route call returns a plan; the SDK turns that plan into approve and Pool.swap legs; the wallet submits them as an EIP-5792 batch or as sequential transactions. Route SELECTION never happens on chain: it is a global search over every pool’s curve, coverage and marks, and running it on chain would cost more than the spread it saves. A chosen path can be settled leg by leg as above, or handed whole to Router.swap for one signature and an all-or-nothing fill (Composability §2). Venues, and therefore plans, resolve per chain.


1. When to use what

NeedCallThen
Best fill across listed pools on a chainPOST /v1/routeplanToLegsbuildSwapCalls
The same path, one transaction, one signaturePOST /v1/routebuild Part[] + Floor[]Router.swap (example)
Price one leg exactlyPool.getSwapQuote on chain, or POST /v1/quote off chain (canonical sample)Pool.swap
Local estimate with no network round triprankSwap / quoteExactIn (@btr-protocol/sdk/amm, f64)Dev and UI preview only

Single-pool swap() is correct for a fixed venue: an aggregator hop, or arbitrage against one pool. For end-user swap UX, route.

The TypeScript pricer in @btr-protocol/sdk/amm is an f64 replica of the integer kernel. It is close, not identical. Anything that settles value should price off Pool.getSwapQuote (exact, on chain) or POST /v1/quote (exact, the same integer kernel off chain; canonical sample).


2. Pipeline

read pool state on chain

POST /v1/route

SwapPlan / legs

planToLegs (applies per-leg minOut)

buildSwapCalls: wrap? approve* swap* unwrap?

wallet_sendCalls, or N sequential txs

Pool.swap pulls tokenIn from msg.sender. Two consequences:

  1. Multicall3 cannot run swaps. Under Multicall3.aggregate3, msg.sender is the Multicall3 contract, which holds neither the balance nor the allowance. Calls must originate from the user’s own account.
  2. Every leg of a split is funded from the user directly, so each needs its own approval.

3. HTTP

Host and full path list: API & SDK Reference.

POST /v1/route is a stateless ranking kernel. It holds no pool state: you supply the pricing state of every pool you want considered, and it returns the best single route and the best split across them. Reading that state on chain is your job.

curl -s https://api.btr.markets/v1/route \ -H 'content-type: application/json' \ -d '{ "pools": [{ "tag": "btr-crypto", "base": "USDC", "base_decimals": 6, "spokes": [ /* SpokeWire, each with decimals or address */ ] }], "token_in": "USDC", "token_out": "WETH", "amount_in": "0x…" }'

Field names are snake_case, and every 256-bit value is a 0x hex string; no JSON numbers on the trust boundary. Wire rules, the full NamedPoolWire / SpokeWire shape and the response schema are canonical in API & SDK Reference §3 and not restated here.

Every leg needs its decimals resolvable (rule and error shape). Passing decimals explicitly is the cheaper path; the resolver otherwise has to look each address up.

The response ranks routes; it computes no minOut. Slippage is applied client-side, per leg (§5).


4. TypeScript

import { planToLegs, buildSwapCalls } from '@btr-protocol/sdk/router'; import { poolStateFrom } from '@btr-protocol/sdk/amm'; // 1. Addresses. /v1/venues is the source of truth: never hardcode. const venues = await fetch('https://api.btr.markets/v1/venues').then((r) => r.json()); const chain = venues[chainId]; // 2. Read pool state on chain, then POST it for ranking. const plan = await fetch('https://api.btr.markets/v1/route', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pools, // NamedPoolWire[] you assembled token_in: 'USDC', token_out: 'WETH', amount_in: `0x${amountIn.toString(16)}`, }), }).then((r) => r.json()); // 3. Plan → legs → calls. const legs = planToLegs(plan, { slippageFrac: 0.005, // 50 bps per leg tokenOf: (sym) => chain.tokens[sym], nativeIn: true, // user pays in the gas token amountInUnits: amountIn, // exact bigint - see below }); if (!legs) throw new Error('unroutable: missing pool address or token meta'); const calls = buildSwapCalls(legs, { recipient: user, wrappedNative, // NOT served by /v1/venues - see below deadline: BigInt(Math.floor(Date.now() / 1000) + 600), }); // 4. EIP-5792 where the wallet supports it; otherwise send calls[i] in order. await wallet.sendCalls?.({ calls: calls.map((c) => ({ to: c.to, data: c.data, value: c.value })), });

The SDK exported routeAsync and quoteExactInAsync as thin wrappers over these two endpoints. Both were removed; the failure mode is documented once, in API & SDK Reference §5.2. Build the request body yourself; the shape is in §3 above. planToLegs and buildSwapCalls are unaffected.

wrappedNative does not come from /v1/venues. That endpoint’s contracts map has 14 keys (ac, admin, adminImpl, deployer, faucet, flash, flashImpl, guardian, oracle, owner, poolFactory, poolImpl, refOracle, treasury) and no wrapped-native entry. Reading chain.contracts.wnative yields undefined, and buildSwapCalls then builds a wrap call to nowhere. Source the wrapper address yourself and only when you actually set nativeIn / nativeOut. Note that on Arc the gas token is native USDC, so the wrap step most chains need does not apply to USDC legs at all.

Always pass amountInUnits. Without it the input leg is rebuilt from plan.amountIn, an f64 that cannot hold 18 decimals: a balance of 31049999999999999999 wei round-trips to 31050000000000000000, one wei above the balance, and transferFrom reverts. The approval is built from the same inflated figure, so it matches and hides the cause.

Single pool, no route:

import { getSwapQuote, swap, defaultDeadline } from '@btr-protocol/sdk/pool'; const q = await getSwapQuote(provider, pool, tokenIn, tokenOut, amountIn); const minOut = (q.amountOut * 9950n) / 10000n; // 50 bps await swap(provider, pool, { tokenIn, tokenOut, amountIn, minAmountOut: minOut, recipient: user, deadline: defaultDeadline(), });

5. Depth and slippage

POST /v1/depth builds a ladder for a UI order book from the same pricing kernel as /v1/quote. Its rows are JSON floats for rendering, not settlement arithmetic.

Slippage is applied per leg, never once across the plan:

  • planToLegs floors each leg at slippageFrac of its quoted output. applySlip is the same helper if you are flooring amounts yourself; it resolves from @btr-protocol/sdk/router, /utils and the package root alike.
  • Splits fund each part from the user, so each part carries its own independent minOut.
  • Two-hop parts use leg 1’s minOut as leg 2’s amountIn. That is deliberately conservative: leg 1 usually returns more than its floor, and the remainder stays with the user rather than being stranded in the second hop.
  • refloorLeg re-derives a leg’s floor against a fresh quote, for plans that sat long enough to go stale.

Sizing guidance and the tolerance bands: Basic Operations §7. Mechanics of the spread itself: Slippage & Price Impact.


6. LP dual routes

Mint and redeem each have two viable strategies, and which one wins depends on live inventory. rankDeposit and rankRedeem (@btr-protocol/sdk/router) rank them; buildDepositCalls and buildRedeemCalls turn the winner into calls.

MintRedeem
market-first: swap then depositwithdrawTo: cross-asset exit in one call
deposit-first: deposit then swapLiabilityswapLiability then withdraw

rankDeposit returns a single ranked plan object, not an array. Worked examples: Cookbook §3.