API & SDK Reference

Public, unauthenticated, read-only HTTP API. Writes happen on chain from the user’s own wallet: no key, no authenticated surface. Every REST resource is under /v1; three infra paths are not (§2). Below: the path list, the request shapes of every POST, the error model, and the @btr-protocol/sdk entrypoints that resolve.


1. Hosts

https://api.btr.markets/v1 # canonical: use this in all examples

REST: https://api.btr.markets/v1/*. WebSocket: wss://api.btr.markets/stream.

OpenAPI 3.1: https://btr.markets/openapi.json, served from btr.markets only (api.btr.markets/openapi.json is a 404). It covers every path in §2 with the request and response schemas below. https://data.btr.markets also resolves to the gateway, but is not the canonical host; do not build calls against it.

Infra-only ingress rewrites, not user URLs: btr.markets /api/* → gateway, btr.markets /docs-api/* and /blog-api/* → docs service.

Every chain-scoped response carries a chainId. GET /v1/venues has one entry per registered chain; GET /v1/chains says which are live. Write chain handling as a lookup over that map, never a pinned id.

1.1. Conventions

PropertyValue
AuthNone. No key, no header, no signed request
Rate limitPer client IP, 600 requests per 60 s window, one budget shared by /v1/md/*, /v1/assets (GET and POST), the chain quotes (/v1/chain/quote, /v1/chain/route) and the book POSTs (/v1/quote, /v1/quote-path, /v1/route, /v1/depth, /v1/indicators, /v1/lp-rows), plus /v1/pools/{tag}/params on an indexer miss. Over budget: 429 {"error":"rate_limited"} with Retry-After in seconds. Other paths are unmetered. No RateLimit-* headers
CORSAllowlist, not wildcard; see below
CachingGET responses are edge-cached for a few seconds; polling faster returns the same body
VersioningOne version, /v1, pinned in the path. Fields are added, never removed or retyped. A breaking change would ship under a new major prefix beside /v1, never in place
DeprecationRFC 8594 Deprecation / Sunset headers, sunset ≥180 days out. Nothing is deprecated today, so no such header is emitted

1.2. CORS is an allowlist

The gateway answers an allowlisted Origin with a matching Access-Control-Allow-Origin. Any other Origin gets 403 {"error":"origin not allowed"}.

Origin: https://btr.markets → access-control-allow-origin: https://btr.markets Origin: http://localhost:3000 → access-control-allow-origin: http://localhost:3000 Origin: https://your-app.com → 403 {"error":"origin not allowed"}

Local development against http://localhost:3000 works; a deployed third-party front end calling the gateway directly from the browser does not. Proxy through your own backend, or ask to have your origin allowlisted. Server-side calls (cURL, Node, Rust, Python) are unaffected: CORS is a browser policy, not a server restriction.

1.3. Error model

Most errors are a JSON object with a single error key:

{"error": "unknown preset 'close'"}

Some are text/plain. Do not assume a parseable body; check content-type, or guard your .json() in a try.

StatusMeaningBody
400Malformed request: unparseable body, or a missing or rejected query valueJSON on /v1/quote, /v1/route, /v1/lp-rows, /v1/timeseries, /v1/oracle/history, /v1/indicators (tf off the supported ladder, or from not in the past); plain text on /v1/depth (unparseable body), /v1/md/ohlc (tf missing), /v1/indicators (symbol or preset missing) and GET /stream
403Browser Origin not on the allowlist (§1.2)JSON
404No such path, wrong method (the POST endpoints 404 on GET), unregistered chain, unindexed resource, or an unknown symbol on /v1/indicatorsJSON; plain text unknown symbol on /v1/md/ohlc/{sym}
422Well-formed request the service refuses: a halted or uncertain leg (/v1/quote, /v1/quote-path), {"error":"no_route","reason":"capacity"} (/v1/route), no depth for pair (/v1/depth), an unknown preset, a GET /v1/indicators series whose newest bar is older than staleAfterSec ({"error":"stale series","ageSec":…,"staleAfterSec":…}, no rows), or a body that failed to deserialize on /v1/depthJSON; plain text for the /v1/depth deserialize failure
408A POST body not fully read within 5 sJSON
413A POST body over 1 MiBJSON
414A request target over 8192 bytesJSON
429Over the per-IP budget (§1.1)JSON {"error":"rate_limited"}, Retry-After in seconds
502Upstream failure, or an upstream body over 2 MiB. Not always retryableplain text, error code: 502
503A chain registered but not deployed on a chain-scoped read, {"chainId":<id>,"error":"chain <id> not live"}; route planning past its 2 s deadline; /v1/chain/* RPC unavailable (rpc_unavailable: …); a full request lane: {"error":"busy"} with Retry-After: 1 (bulk busy, quote busy, bulk response limit on the bulk and forward paths). Retry after the delayJSON
501/v1/chain/route with max_hops above 2 or split: trueJSON
504A quote-service request past its deadline (12 s on /v1/chain/*, 10 s on the book POSTs): {"error":"deadline"}. Retry; it is not a verdict on liquidityJSON

Request limits that answer 400: /v1/route and /v1/depth refuse more than 64 pools, more than 256 spokes in one pool, more than 512 spokes in total or over 2,000,000 units of estimated search work (planner work limit) and token names over 128 characters (token name limit); /v1/lp-rows refuses more than 256 legs (too many LP legs); /v1/timeseries and its bulk form refuse more than 10,000 points per query or 100,000 per batch (timeseries work limit), over-long or too many query parameters (query parameter limit) and an unparseable from/to (invalid time bound). /v1/chain/route with the same token on both sides is 400 (tokens must differ), not 422.

Status codes are not uniform across the POST endpoints for the same class of failure: a missing-field body returns 400 (JSON) on /v1/quote, /v1/route and /v1/chain/*, 422 (plain text) on /v1/depth. Branch on the status you got, not on the endpoint family. Deserialization failures name the first missing field, so iterate on the message.

A structurally valid request is priced as sent. mark: "0x0" returns 200 with every amount 0x0, and a curve the pricer rejects is 400 {"error":"InvalidInput"}. Reject a zero mark before sending; the chain reverts on it.


2. Path surface

Served by the gateway at https://api.btr.markets. Paths not listed here are internal and unsupported. /health, /ready and /stream are unversioned on purpose: the first two are probes every BTR service answers at the same path, and /stream frames carry a type, so a new frame is a new type, not a new URL. Chain-scoped routes take ?chainId= (decimal; absent: the default chain; unparseable: 400; unregistered: 404; registered, not deployed: 503).

2.1. GET

PathReturnsQuery
/healthGateway liveness: 200 once serving, 503 while starting-
/ready200 when the market-data stream is connected, else 503-
/v1/healthIndexer health for one chain: feed freshness, asset coverage, ingest cursor, every served chain’s readinesschainId
/v1/md/tickersCurrent mark per provider ticker (§4.1)-
/v1/md/tickers/detailProvider ticker id to base/quote-
/v1/md/ohlc/{sym}OHLC barstf (required, seconds), from, to (epoch ms, snapped to the bar grid), limit
/v1/md/tickers/detail/{sym}Base/quote for one provider ticker-
/v1/md/price/{sym}Latest price for one symbol-
/v1/indicatorsOne technical-indicator seriessymbol, preset (both required); tf (default 30), fast, slow, signal, from (window start, epoch ms; default and cap: the newest 10 000 tf buckets, at most 30 d)
/v1/assetsMetadata for named assetsaddresses (required)
/v1/assets/poolsDeployed pools, spokes and LP receiptschainId
/v1/assets/icons/{file}Token and network icons (SVG, PNG)-
/v1/activityRecent swap, deposit and withdraw eventslimit (1–200, default 200), before, pool, asset, action, payer, receiver, token_in, token_out
/v1/liquidityDeposits and withdrawals bucketed per pool, symbol and sidepool, asset, side (deposit, withdraw), grain, from, to
/v1/abis/{name}Lean ABI array for one contract-
/v1/venuesDeployed contracts, tokens, feeds and pools, keyed by chain id-
/v1/summaryProtocol and per-pool totals: TVL, volume, fees, coverage toll (tollUsd, LP revenue outside lpFeesUsd), and at protocol, pool and asset scope feeApr, strategyApr (hook), tollApr (memo, not in the total), apr (fee + hook), apy (compounded; null above 100% APR or under a 24h window, read apr then) and liabilitiesUsd (the rate base). Assets add hooked, venueApr, windowMs, hookWindowMs. null is unknown, never 0window (<n>{m,h,d,w} or ms, 1h..365d; default 7d, the same window /v1/pools/metrics uses)
/v1/poolsDeployed pools + symbol rosters-
/v1/pools/metricsThe /v1/summary yield fields, one row per pool assetwindow (with a unit, default 7d)
/v1/timeseriesOne metric bucketedmetric (required; an unknown one is a 400 listing all; skew.psi, which never served data, is removed for skew.bps), pool, asset, by=asset, grain (1m to 1d, never finer than the span allows), from, to (epoch ms, span at most 1 year, default 48 h)
/v1/oracle/historyFeed price history, newest firstfeed (required): feed name (WETH-USDC) or feedId; a bare symbol or tickerId returns 404. from, to, limit (1 to 2000, default 200)
/v1/oracle/feedsThe chain’s live oracle feed registry: name, symbol, token, feedId, tickerId (decimal string), tickerIdHex and decoded MITCH fields, served from the baked deployment records (unregistered chain: 404; registered, not deployed: 503)chainId
/v1/chainsServed chain ids, each with default and live flags-
/v1/safety/historyHalt and wedge events, decoded, newest first. An unreadable store is 503, never an empty listfrom, to (default 30 d), limit (1 to 2000)
/v1/safety/guardsWhat the guards keeper attempted, dry runs included (/v1/safety/history shows what landed)chainId, from, to (default 30 d), limit (1 to 2000)
/v1/safety/breakersThe automatic breakers: latest gates, trip historychainId, from, to (default 30 d), limit (1 to 2000)
/v1/oracle/pushesFeed push history, paged by a before cursorfeed, tier, before, limit, chainId
/v1/oracle/pushes/{tx}One push’s signed payload, by transaction hashchainId
/v1/pools/{pair}/densityFitted target density for one leg, e.g. WETH-USDCwindow (14d default, 7d)
/v1/pools/{tag}/paramsOne pool’s live risk parameters and every leg’s, read at one block in display units (bps, whole tokens, named flags). A leg’s oracle.gate includes its ref feed (refGate) when the band is armed. fitThetaBps is the density fit’s recommended θ, keyed by symbol across chains and null once fit.stale; it is not the keeper’s running value. Cached 5 s per pool. Unknown tag: 404; a failed chain read: 502chainId

{name} on /v1/abis serves Pool, Admin, Flash, PoolFactory, AccessControl, LPToken, MarkStore and ExternalOracleV4 (legacy oracle, read-only); any other name, Router included, returns 404 {"error":"no such abi"}. The ABIs track current source (Contract Addresses). The same ABIs ship statically in @btr-protocol/sdk/abis (§5.1), a zero-server-trust surface.

2.2. POST

PathPurpose
/v1/chain/quoteOne pool’s getSwapQuote from the chain at 1 to 64 sizes, with the server’s floor (§2.4)
/v1/chain/routeBest itinerary from the chain, with end-to-end floors (§2.4)
/v1/quotePrice one exact-in leg against supplied leg state
/v1/routeRank routes and splits across supplied pool states
/v1/depthBuild a depth ladder across supplied pool states
/v1/lp-rowsRanked LP exit/entry rows plus the debt-swap destination, from supplied pool state (§3.4)
/v1/timeseries/bulkUp to 200 timeseries queries in one request (§3.5)
/v1/quote-pathPrice a one- or two-leg path, settled once as the chain settles it (§3.1)
/v1/indicatorsOne indicator series over caller-supplied bars (§3.5)
/v1/assetsMetadata for up to 200 addresses (§3.5)

GET on /v1/chain/quote, /v1/chain/route, /v1/quote, /v1/quote-path, /v1/route, /v1/depth, /v1/lp-rows or /v1/timeseries/bulk returns 404, a method mismatch, not a missing endpoint. /v1/indicators and /v1/assets serve both methods.

2.3. WebSocket

/stream: live price batches at wss://api.btr.markets/stream. A plain GET returns 400 because the request is not an upgrade. Send {"type":"subscribe","symbols":["ETH-USDC"]} (case and hyphen insensitive, agg:spot: prefix optional; at most 128 per socket, within 30 s of connecting or the socket closes); {"type":"unsubscribe","symbols":[…]} drops them and {"type":"ping","t":…} answers {"type":"pong","t":…}. Updates arrive as {"type":"price_batch","updates":[{"symbol","pair","price","bid","ask"}]}, primed with the current quote on subscribe, then only moved quotes. At most 8 sockets per IP.

2.4. Chain quotes (/v1/chain/*)

Token addresses in. The service resolves the pool from its deployment records and reads every number from Pool.getSwapQuote with one Multicall3.aggregate3 pinned to one block (block in the response): one call on quote, one per hop layer on route. It authors the slippage floor from the same quote. This is the surface the app prices on.

slippage is one of:

  • {"mode":"spread","pct":10..300}: tolerance as a percentage of the quote’s own spread.
  • {"mode":"relative","pbps":100..999000}: a fixed tolerance.

Out of range is a 400, not a clamp. Every floor satisfies min_out = amount_out·(1e6 - tol_pbps)/1e6.

POST /v1/chain/quote

FieldTypeNotes
token_in, token_outstringToken addresses; a symbol is 400 unknown token
amounts_in0x hex[]1 to 64 non-zero sizes, native raw units
slippageobjectSee above
poolstring, optionalPool tag (btr-stable). Required when two pools hold the pair: 422 ambiguous_pool otherwise
chain_idu64, optional?chainId= wins; absent both: the default chain
curl -s 'https://api.btr.markets/v1/chain/quote?chainId=<chainId>' \ -H 'content-type: application/json' \ -d '{"token_in":"0x…","token_out":"0x…","amounts_in":["0xf4240"], "slippage":{"mode":"spread","pct":50}}'
{ "chain_id": <chainId>, "block": {"number": 0, "timestamp": 0}, "source": "chain", "amounts": [{ "amount_out": "0x…", "spread_pbps": 0, "proto_fee": "0x…", "lp_fee": "0x…", "cov_toll": "0x…", "mark_price": "0x…", "mid_price": "0x…", "route_hops": ["0x…"], "hop_amounts": ["0x…"], "tol_pbps": 0, "min_out": "0x…" }], "flags": {"in": 0, "out": 0, "swap_enabled": true, "halted": false, "swap_gated": false, "open_path": true} }

One slot per size, in request order. A size the pool refuses is {"refused":{"name","selector","args"}} in its slot (a revert, or zero_out), never a zero price. swap_gated is true when either token carries the swap allowlist flag: only an executor holding AccessControl.perms bit 10 can fill it, anyone else reverts NotAuthorized. open_path is true when both tokens admit an executor without it. The response header x-btr-witnessed is 1 when a second RPC operator corroborated the answer, 0 when not. Direct pool only: a pair no single pool holds is 422 no_route; take /v1/chain/route.

POST /v1/chain/route

FieldTypeNotes
token_in, token_outstringToken addresses
amount_in0x hexNon-zero, native raw units
slippageobjectSee above
max_hops1 or 2, optionalDefault 2. Above 2 is 501
splitbool, optionaltrue is 501
chain_idu64, optionalAs on quote
{ "chain_id": <chainId>, "block": {"number": 0, "timestamp": 0}, "source": "chain", "best": { "parts": [{"hops": [{"pool": "0x…", "token_in": "0x…", "token_out": "0x…", "amount_in": "0x…", "amount_out": "0x…"}]}], "floors": [{"token": "0x…", "amount_out": "0x…", "tol_pbps": 0, "min_out": "0x…"}] }, "singles": [{"hops": []}], "refused": [{"name": "…", "selector": "0x…", "args": []}] }

best is direct or two hops through the hub, null when nothing fills. floors is end-to-end, one per output token: encode it as the swap’s minOut, never a per-hop figure. singles ranks every priced itinerary; refused names each candidate the pool refused (halt, swap disabled, coverage). flags lists every {pool, token} flag word read at block; swap_gated and open_path are set over best’s legs, false when best is null. Past the 12 s deadline: 504.

Errors on both: 400 (body, token, slippage), 422 (no_route, ambiguous_pool), 429, 501, 503 (rpc_unavailable: …, or the chain not live), 504 (deadline).


3. The pricing POST endpoints are stateless kernels

/v1/quote, /v1/quote-path, /v1/route and /v1/depth hold no pool state; /v1/chain/* (§2.4) reads the chain instead. You do not pass a pool address and get a price back. You pass the entire pricing state of every leg involved (curve, reserves, liabilities, mark, σ, confidence, staleness, fee params) and the service runs the same integer pricing kernel the contracts run. It is the on-chain pricer, callable off chain, on state you supply.

  • Reading pool state is your job, on chain. /v1/venues gives the addresses; the SDK’s storage readers (@btr-protocol/sdk/pool) give the slots.
  • The kernel is authoritative for arithmetic, not for freshness. Stale inputs produce a wrong number with no warning.
  • All 256-bit values cross the wire as 0x hex strings. A JSON number above 2^53 loses precision and JavaScript stringifies large integers as 1e+21, which the deserializer rejects.
  • Field names are snake_case (token_in, amount_in, sigma_pbps).

3.1. POST /v1/quote

Required body fields, all at the top level:

FieldTypeNotes
curveobject{ header: string, segs: SegWire[], m: number }; SegWire = {c0,c1,c2,c3,c4,s}, all 0x hex
min_dispersion_pbpsu32
vega_bpsu16
min_fee_pbpsu32
kappa_cov_bpsu16
amount_in0x hex
reserves0x hex u128native raw units of the token
liabilities0x hex u128native raw units
mark0x hex U256WAD (1e18)
sigma_pbpsu32
sellingbool
counterpartyobject{reserves, liabilities, vega_bps, kappa_cov_bps}: the swap’s other endpoint; see below
confidence_bpsu16The feed’s mark CI. Absent, null or above 1,000 (MAX_CONFIDENCE_HALT_BPS) returns 422 {"error":"leg uncertain: …"}, as the pool reverts on it
stale_excessu32seconds of staleness beyond TTL
proto_share_pctu8
flagsu16, optionalthe feed’s flag word; bit 0 is the guardian halt. Default 0. A halted leg is refused (422), not quoted
lpobject, optionalLP preview for this leg’s own book: deposit/withdraw a raw amount and get back post-trade coverage and diluted APR alongside the swap price

The canonical request shape, once per corpus (other pages link here, never restate it):

curl -s https://api.btr.markets/v1/quote \ -H 'content-type: application/json' \ -d '{ "curve": {"header": "0x…", "segs": [], "m": 0}, "min_dispersion_pbps": 100, "vega_bps": 1, "min_fee_pbps": 1, "kappa_cov_bps": 100, "amount_in": "0x…", "reserves": "0x…", "liabilities": "0x…", "mark": "0x…", "sigma_pbps": 10000, "selling": true, "counterparty": { "reserves": "0x…", "liabilities": "0x…", "vega_bps": 10000, "kappa_cov_bps": 600 }, "confidence_bps": 5, "stale_excess": 0, "proto_share_pct": 20 }'

counterparty: a leg is not a path

The body above describes one leg. The contract prices a path, and its settle tail reads two things off the path’s two endpoints that no leg carries:

  • acc.vegaBps = max(cIn.vegaBps, cOut.vegaBps): the spread’s vega is the endpoint maximum, not the walked leg’s own value.
  • _covToll(cOut, …): the convex coverage toll is charged on whichever endpoint the swap delivers. On a spoke→base sell that endpoint is the hub, which is not exempt: every listed asset including the base runs 50 ≤ κ ≤ BPS.

counterparty is that far-side endpoint, the pool’s base on a direct spoke↔base swap, in that token’s native raw units. Required; a body without it is a 400. There is no default: the only candidate (a zero endpoint) silently drops the hub’s coverage toll and quotes a price the pool will not fill, a gap reaching hundreds of bps at size on the live fleet.

A cross (spoke→base→spoke) is one path, not two legs: the chain charges the path spread once, at the tail, so two summed /v1/quote calls miss its figure by ~4 bps. Price it with POST /v1/quote-path: {"legs": [sell, buy]}, each a /v1/quote body plus decimals_in and decimals_out, at most two legs. Neither leg’s counterparty is read on that path; send {"reserves":"0x0","liabilities":"0x0","vega_bps":0,"kappa_cov_bps":0} on both. /v1/quote refuses that all-zero endpoint: 400 {"error":"missing counterparty: explicit zero endpoint is not a book"}.

Response:

{ "amount_out": "0x…", "gross_out": "0x…", "avg_price": "0x…", "mid_price": "0x…", "mark_price": "0x…", "spread_pbps": 0, "cov_toll": "0x…", "proto_fee": "0x…", "lp_fee": "0x…", "saturated": false, "premium_bps": 0, "impact_bps": 0, "fee_bps": 0, "toll_bps": 0, "cost_bps": 0, "coverage_pre": null, "coverage_post": null, "fee_apr_after": null, "strategy_apr_after": null }

The trailing float fields are derived display metrics over the same fill (premium_bps mid vs mark, impact_bps curve slippage before fees, fee_bps/toll_bps the fee and toll each over gross, cost_bps the signed whole taker cost); the coverage_*/*_apr_after quartet is null unless the request carried lp, in which case they preview that deposit or withdraw against this leg’s book.

saturated: true means the gross output was clamped at the delivering book’s coverage argmax: amount_out is the flat top of the wall, and more input buys no more output. Treat that size as unfillable, not as a price. Past a band of 2% of that book’s liabilities above the argmax the request fails instead, as the chain reverts.

There is no min_out. Derive your own slippage floor from amount_out; see Basic Operations §7.

3.2. POST /v1/route

FieldTypeRequired
poolsNamedPoolWire[]yes
token_instringyes
token_outstringyes
amount_in0x hex U256yes
slicesu32no
min_gain_bpsu64no
max_routesusizeno
max_hopsusizeno; default 2
split"auto" | "off" | "on"no; default auto
costs{gas_per_leg_out?, gas_per_part_out?, approval_per_part_out?}, each 0x hex in out-token raw unitsno; omitted ranks on gross output

NamedPoolWire = { tag, base, spokes: SpokeWire[] } plus addr, base_address, base_decimals and the hub’s endpoint book: base_reserves, base_liabilities (both 0x hex u128), base_vega_bps and base_kappa_cov_bps.

The four hub fields are the /route and /depth form of counterparty above, and they are all-or-nothing. Report the whole book or every direct leg touching your base, sell and buy, quotes zero and leaves the route set: /v1/route answers 422 {"error":"no_route","reason":"capacity"} and /v1/depth 422 {"error":"no depth for pair"} when nothing else reaches the pair. Each missing field would drop a charge the chain makes.

base_address and base_decimals are individually optional, not jointly omissible: a wire carrying neither is rejected with 400 {"error":"USDC: wire omits both base_decimals and base_address"}. Supply at least one so the decimal resolver can work. Same per spoke.

SpokeWire = { token, pricing, reserves, liabilities, mark, sigma_pbps, confidence_bps, stale_excess, proto_share_pct } plus address, decimals and an optional flags (bit 0 = guardian halt, default 0): a halted spoke drops out of the routing universe and out of /depth, the same way an omitted confidence_bps does. pricing is the same five-field object that heads a quote request: { curve, min_dispersion_pbps, vega_bps, min_fee_pbps, kappa_cov_bps }. Give each spoke decimals or address: omitting both fails the same way the base does.

Response:

{ "best_amount_out": "0x…", "best_net_amount_out": "0x…", "best_is_split": false, "best_parts": [{ "legs": [], "fraction": "0x…", "amount_out": "0x…" }], "singles": [{ "legs": [], "amount_in": "0x…", "amount_out": "0x…", "net_amount_out": "0x…", "hops": 1 }], "metrics": { "mid": 0, "mark": 0, "gross": 0, "avg": 0, "impact_bps": 0, "fee_bps": 0, "toll_bps": 0 }, "routes_considered": 1, "halted": [] }

best_net_amount_out is best_amount_out less the request’s CostWire (gas/approval terms); it equals best_amount_out when no costs were sent, and is the ranking key rather than raw output. halted names every pool_tag:token spoke dropped from the routing universe: feed halted (flags bit 0), or confidence_bps absent or above 1,000. metrics are the best plan’s display floats; each single carries its own.

LegWire = { pool_tag, token_in, token_out, amount_in, amount_out, saturated }, where saturated has the /quote meaning above for any hop of that leg. No minOut is computed anywhere in the response; it is the caller’s to apply per leg.

3.3. POST /v1/depth

FieldTypeRequired
poolsNamedPoolWire[]yes
fromstringyes
tostringyes
samplesu32no; sweep resolution per side per pool, default 48
max_hopsusizeno; same clamp /v1/route applies, default 2

Response: { mark, mid, bid, ask, premium_bps, cost_bid_bps, cost_ask_bps, oev_bps, step, bids: Row[], asks: Row[], poolCount }, Row = { price, size, cum }. cost_bid_bps / cost_ask_bps / oev_bps are null when nothing is takeable on that side. These are JSON floats; this endpoint is for rendering a book, not for settlement arithmetic.

3.4. POST /v1/lp-rows

Ranks exit and entry rows for LP moves over the legs you send, in asset units (not raw).

FieldTypeRequired
legs{symbol, reserves, liabilities, haircut_frac, fee_apr?, strategy_apr?, flags?, index_wad?, cap?}[]yes
amountf64 face amountno
mode"coverage" | "apr"no; default coverage
entry"mint" | "liability"no; default mint
excludestringno
src, src_capdebt-swap source leg and its ceilingno
limitrows per listno; hard cap 32
pool_c_wad0x hex WAD, pool coverage R/Lno

Response: { exit_rows: Row[], lp_rows: Row[], destination: string | null }, Row = {symbol, coverage, cov_after, apr_after, haircut_frac, feasible, c_degraded}. c_degraded: true means no pool_c_wad was sent and cov_after is the C-blind estimate. destination is the first feasible leg for a debt swap from src, null when the source cannot cover the face. A bad body is 400.

3.5. Other POST bodies

PathBodyResponse
/v1/indicators{bars: {t, open, high, low, close}[], preset, fast?, slow?, signal?}, 1 to 20 000 barsRow array, as the GET’s rows (§4.5). 422 on an unknown preset or bad periods
/v1/assets{addresses: string[]}, at most 200{assets: (Asset | null)[]} aligned with the request, as the GET
/v1/timeseries/bulk{queries: {metric, …}[]}, at most 200, each a map of the GET parameters{series: []} in request order. A URL chainId applies to queries that name none. All failing is 502; a partial batch is 200 with source: "error" on the failed entries

4. Response shapes

4.1. /v1/md/tickers

[{"ticker":288583527640858624,"mid":291807.65,"bid":291221.02,"ask":292394.28, "ci":12367,"confidence":0,"flags":64,"age_ms":77084658,"status":"dead"}]
  1. ticker is the data provider’s numeric instrument id, not a symbol. /v1/md/tickers is the provider’s whole market universe, not the oracle roster. GET /v1/oracle/feeds maps tickerId (a decimal string) to symbol for every listed feed; /v1/pools and /v1/assets/pools carry no ticker ids. Ids are u64 and 13 of them are not exact as JavaScript numbers: read ticker with a BigInt-aware JSON parser.
  2. Gate on status and age_ms. A ticker whose feed has gone stale reports status: "dead" and keeps its last mid. A client that reads mid without checking status prices off a dead feed.

4.2. /v1/activity

{"chainId":<chainId>,"events":[{"id":"0x…:47","kind":"swap","pool":"btr-<name>", "payer":"0x…","amountIn":3.6386154,"amountOut":1.99187305,"logIndex":47}]}

id is txHash:logIndex. Paginate backwards: pass the response’s nextBefore as before.

4.3. /v1/venues

Keyed by chain id as a string (.["<chainId>"] in jq). Each entry carries chain_id, name, contracts, tokens, pools, feeds, feed_meta, ref_feeds and seeds. contracts holds the roles in that chain’s deployment record. There is no wrapped-native key. Same records as Contract Addresses; read addresses from one of the two, never hardcode them.

4.4. Floats on the wire

The GET surface returns JSON floats for prices and amounts (mid, amountIn, amountUsd): display values. Exact integer arithmetic lives on the POST kernels and on chain, where every 256-bit value is 0x hex. Never settle against a GET float.

4.5. Indicator presets

preset accepts exactly: ema-trend, emacd, rsima, rsimacd, adxma, adxmacd, sdevma, sdevmacd.

Anything else returns 422 {"error":"unknown preset '…'"}.


5. SDK

@btr-protocol/sdk: TypeScript source on GitHub, not on npm and not built. It runs under Bun or a bundler that compiles TS; plain Node refuses it (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING).

bun add github:btr-protocol/sdk bun pm trust @btr-protocol/sdk

The postinstall (bun run fetch-abis) writes src/abis/Pool.ts and Admin.ts from the committed, hash-pinned snapshot: it needs bun, not the network. Bun blocks dependency postinstalls by default, hence bun pm trust. Until it has run, the package root, /pool, /router and /abis fail with Cannot find module './Admin.js'.

5.1. Reachable entrypoints

Once installed as above, these subpaths resolve. Nothing outside this table is importable, whatever a file in the source tree appears to export.

ImportSurface
@btr-protocol/sdkRe-exports ./pool, ./router, ./amm, ./venues, ./oracle, ./eth, ./types, the ./utils helpers, the chain client (chainQuote, chainRoute, ChainError, resetChainClientState) and setApiRoot, getApiRoot, withChainId
@btr-protocol/sdk/poolgetSwapQuote, swap, deposit, withdraw, getPoolData, getAsset, getCoverageRatio, getLPBalance, defaultDeadline, NATIVE_TOKEN, POOL_ABI, storage readers, types
@btr-protocol/sdk/routerplanToLegs, buildSwapCalls, buildApprovalCalls, buildSwapExecCalls, planToRouterPlan, buildRouterCalls, buildRouterApprovalCalls, buildRouterSwapExecCalls, buildDepositCalls, buildRedeemCalls, totalValue, rankDeposit, rankRedeem
@btr-protocol/sdk/ammWire and curve types (SwapPlan, NamedPoolWire, SpokeWire, CurveWire) and curve helpers, including poolStateToWire. The f64 pricers (quoteExactIn, rankSwap) are removed; price via /v1/chain/*, /v1/route or Pool.getSwapQuote
@btr-protocol/sdk/abisPOOL_ABI, ADMIN_ABI, FLASH_ABI, POOL_FACTORY_ABI, ACCESS_CONTROL_ABI, LP_TOKEN_ABI, POOL_HOOKS_ABI, ROUTER_ABI, MARK_STORE_ABI, EXTERNAL_ORACLE_V4_ABI. Oracle reads (getFeed, feedOf, laneOf) go to PoolFactory through POOL_FACTORY_ABI; MARK_STORE_ABI is the quorum-only writer side on the Pool implementation; the V4 ABI is the legacy oracle, read-only
@btr-protocol/sdk/ethContract, RPC client helpers
@btr-protocol/sdk/eth/walletsWallet helpers
@btr-protocol/sdk/oracleSigned-quote decode / digest / recover / quorum
@btr-protocol/sdk/governanceTimelock schedule helpers (delayOf, govDelays, hasZeroDelay), acGeneration, holds and ACCESS_CONTROL_LEGACY_ABI
@btr-protocol/sdk/venuesStatic deployment registry
@btr-protocol/sdk/utilsapplySlip, maths / format / encoding helpers
@btr-protocol/sdk/utils/format · /utils/logger · /typesFormatting, logging, shared types

Not importable: btrFetch and fetchAbi exist in the source tree but no index re-exports them. Fetch ABIs and venues with plain fetch against the paths in §2.1:

const API = 'https://api.btr.markets'; const poolAbi = await fetch(`${API}/v1/abis/Pool`).then((r) => r.json()); const venues = await fetch(`${API}/v1/venues`).then((r) => r.json());

applySlip is re-exported from @btr-protocol/sdk/utils and the package root; both resolve to the same function. The router module uses it internally but does not re-export it.

5.2. Signatures

Every pool function takes an EIP-1193 provider as its first argument (window.ethereum or equivalent, not a viem client) and the pool address as its second.

getSwapQuote(provider, pool, tokenIn, tokenOut, amountIn: bigint): Promise<SwapQuote> getCoverageRatio(provider, pool, token): Promise<bigint> getLPBalance(provider, pool, user, token): Promise<bigint> getPoolData(provider, pool, tokens: Array<{ address: Address; symbol: string; name: string }>, poolName: string): Promise<PoolData> swap(provider, pool, { tokenIn, tokenOut, amountIn, minAmountOut, recipient, deadline? }): Promise<Hex> deposit(provider, pool, { token, amount }): Promise<Hex> withdraw(provider, pool, { token, lpAmount, minAmountOut, deadline? }): Promise<Hex>

deadline defaults to defaultDeadline(), 600 seconds out. To opt out, pass a far-future literal such as 0xffffffffn: the contract only checks block.timestamp > deadline.

Routing:

planToLegs(plan: SwapPlan, { slippageFrac, tokenOf, isOfficialPool, nativeIn?, nativeOut?, amountInUnits?, serverFloors? }): ExecLeg[] | null buildSwapCalls(legs: ExecLeg[], opts: BuildOpts): ExecCall[]

Chain quotes:

chainQuote(req: ChainQuoteRequest, opts?: ChainClientOpts): Promise<ChainQuoteResponse> // POST /v1/chain/quote chainRoute(req: ChainRouteRequest, opts?: ChainClientOpts): Promise<ChainRouteResponse> // POST /v1/chain/route

chain_id is required on both requests; opts takes signal, timeoutMs (default 15 s) and now. Every failure throws ChainError with a closed kind: bad_request, no_route, rate_limited, rpc_unavailable, deadline, not_implemented, refused (the pool refused a size), not_found (a 404: unserved chainId or removed path, not worth a retry), transport, floor_violation (a served floor off min_out = amount_out·(1e6 - tol_pbps)/1e6 or wider than the request’s policy allows), stale_block (an answer from an older block than one already seen for the pair). A 429 or 503 starts one client-wide cooldown for Retry-After seconds; resetChainClientState() clears it and the block guard.

quoteLegAsync, quotePathAsync, routeAsync and depthAsync (@btr-protocol/sdk/amm) are thin clients over POST /v1/quote, /v1/quote-path, /v1/route and /v1/depth. routeAsync and depthAsync are typed on the snake_case wire (RouteRequestWire, DepthRequestWire, both carrying NamedPoolWire[]); legToQuoteBody builds the quoteLegAsync body. Base URL resolves from the explicit argument, else BTR_API, else the SDK default. Inside the SDK, rankDeposit calls routeAsync, the liability quote calls quoteLegAsync and quotePathAsync.

planToLegs and buildSwapCalls work on any plan of the right shape. isOfficialPool is required and fails closed: a plan may name any pool address, official or not, so a plan naming a pool the callback rejects yields null rather than an approve against an unverified address; feed it PoolFactory.isOfficialPool or a set derived from it. serverFloors carries the /v1/chain/* end-to-end min_out per output token; when present the builder encodes that floor and picks no tolerance of its own.

There are no first-class senders for withdrawTo, swapLiability, donate or Flash.flashLoan. Encode those with encodeFunctionData against POOL_ABI / FLASH_ABI from @btr-protocol/sdk/abis, or against a live ABI from /v1/abis.


Quotes & Routing · Cookbook · Basic Operations