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
| Need | Call | Then |
|---|---|---|
| Best fill across listed pools on a chain | POST /v1/route | planToLegs → buildSwapCalls |
| The same path, one transaction, one signature | POST /v1/route | build Part[] + Floor[] → Router.swap (example) |
| Price one leg exactly | Pool.getSwapQuote on chain, or POST /v1/quote off chain (canonical sample) | Pool.swap |
| Local estimate with no network round trip | rankSwap / 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
Pool.swap pulls tokenIn from msg.sender. Two consequences:
- Multicall3 cannot run swaps. Under
Multicall3.aggregate3,msg.senderis the Multicall3 contract, which holds neither the balance nor the allowance. Calls must originate from the user’s own account. - 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
routeAsyncandquoteExactInAsyncas 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.planToLegsandbuildSwapCallsare 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:
planToLegsfloors each leg atslippageFracof its quoted output.applySlipis the same helper if you are flooring amounts yourself; it resolves from@btr-protocol/sdk/router,/utilsand 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
minOutas leg 2’samountIn. 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. refloorLegre-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.
| Mint | Redeem |
|---|---|
market-first: swap then deposit | withdrawTo: cross-asset exit in one call |
deposit-first: deposit then swapLiability | swapLiability then withdraw |
rankDeposit returns a single ranked plan object, not an array. Worked examples: Cookbook §3.