---
title: "API & SDK Reference"
description: "The BTR gateway HTTP surface and the reachable @btr-protocol/sdk entrypoints"
audience: tech
type: reference
status: live
lang: en
updated: "2026-08-30"
publish: true
---
# API & SDK Reference

The BTR gateway is a public, unauthenticated, read-only HTTP API. Writes happen on chain from the user's own wallet, so there is no authenticated surface and no key to obtain. This page is the verified path list, the request shapes for the three POST endpoints, the error model, and the `@btr-protocol/sdk` entrypoints that actually resolve.

---

## 1. Hosts

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

User-facing REST examples use `https://api.btr.markets/v1/*`. A machine-readable **OpenAPI 3.1 spec is published at `https://btr.markets/openapi.json`**. It is served from the `btr.markets` host only (`api.btr.markets/openapi.json` is a 404). `https://data.btr.markets` also resolves to the gateway for market-data WebSocket but is not the canonical REST host; do not build REST calls against it.

**REST:** `https://api.btr.markets/v1/*` (canonical)
**Market-data WebSocket:** `wss://data.btr.markets/stream` (ticks); gateway WS is `wss://api.btr.markets/stream` when documented

Infra-only rewrites (not user URLs): `btr.markets` `/api/*` → gateway and `btr.markets` `/docs-api/*` / `/blog-api/*` → docs service are ingress path-rewrites.

**Everything is Arc testnet (chain id `5042002`) today.** Every response carries `chainId: 5042002`, and `GET /v1/venues` returns a single chain entry. Placeholders for other chains land as they deploy; write your chain handling as a lookup over the `/v1/venues` map rather than pinning one id.

### 1.1. Conventions

| Property | Value |
|---|---|
| Auth | None. No key, no header, no signed request |
| Rate limit | None. No `RateLimit` headers are emitted |
| CORS | **Allowlist, not wildcard**; see below |
| Caching | `GET` responses are edge-cached for a few seconds; polling faster returns the same body |
| Versioning | Major version pinned in the path. Fields are added, never removed or retyped, within `/v1`. A breaking change ships as `/v2` alongside |
| Deprecation | RFC 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 echoes `Access-Control-Allow-Origin` **only for allowlisted origins**. An origin that is not on the list gets no ACAO header at all, and the browser blocks the read.

```
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    → (no header - browser blocks)
```

Local development against `http://localhost:3000` works. **A deployed third-party front end calling the gateway directly from the browser will 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:

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

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

| Status | Meaning | Body |
|---|---|---|
| `400` | Malformed request: unparseable body, or a query value the handler rejected | JSON on `/v1/quote`, `/v1/route`; **plain text** on `/v1/md/ohlc` and `GET /stream` |
| `404` | No such path, wrong method (the POST endpoints 404 on `GET`), or an unindexed resource | JSON |
| `422` | Well-formed request, rejected value: an unknown `preset`, or a body that failed to deserialize | JSON on `/v1/indicators`; **plain text** on `/v1/depth` |
| `502` | Upstream panic. **Not always your fault, and not always retryable**; see below | plain text, `error code: 502` |

Status codes are not uniform across the POST endpoints even for the same class of failure: a missing-field body returns **`400` (JSON)** on `/v1/quote` and `/v1/route`, but **`422` (plain text)** on `/v1/depth`. Branch on the status you actually got, not on the endpoint family.

Deserialization failures name the first missing field, so iterate on the message.

**On `502`.** A structurally valid, fully-populated request can still 502 from an upstream panic on edge-case values. Confirmed triggers on `/v1/quote`: `confidence_bps: 0`, `curve.header: "0x0"`, and `mark: "0x0"`. `confidence_bps: 0` matters in practice: `/v1/md/tickers` serves `"confidence": 0` on this same API, so feeding a ticker straight back into a quote reproduces it. Treat `502` as a bad-input signal first and a transient second: retrying an identical body will not clear it.

---

## 2. Path surface

Verified live on both hosts. Paths not listed here are not part of the API.

### 2.1. GET

| Path | Returns | Query |
|---|---|---|
| `/health` | Liveness and readiness | - |
| `/v1/md/tickers` | Current oracle mark per tracked ticker | - |
| `/v1/md/ohlc/{sym}` | OHLC bars | `tf` (required) |
| `/v1/indicators` | One technical-indicator series | `symbol`, `preset` (both required); `tf` (default 30), `fast`, `slow`, `signal`, `from` (lookback ms, default 30d) |
| `/v1/assets` | Metadata for named assets | `addresses` (required) |
| `/v1/assets/pools` | Deployed pools and their assets | `chainId` |
| `/v1/activity` | Recent swap, deposit and withdraw events | `limit` (1–200, default 200), `before`, `pool`, `asset`, `action`, `payer`, `receiver`, `token_in`, `token_out` |
| `/v1/liquidity` | Liquidity flow bucketed over time | `asset` |
| `/v1/abis/{name}` | Lean ABI array for one contract | - |
| `/v1/venues` | Deployed contracts, tokens, feeds and pools, keyed by chain id | - |
| `/v1/summary` | Protocol totals | - |
| `/v1/pools` | Deployed pools + symbol rosters | - |
| `/v1/pools/metrics` | Per-pool metrics | - |
| `/v1/timeseries` | One metric bucketed | `metric`, `grain` |
| `/v1/oracle/roster` | Oracle address, signer set, event count | - |
| `/v1/oracle/history` | Feed price history | feed selector required |

`{name}` on `/v1/abis` is verified for `Pool`, `Admin`, `Flash`, `PoolFactory`, `ExternalOracleV4`, `ExternalOracle`, `AccessControl` and `LPToken`. `ExternalOracleV4` is the generation the live fleet runs; `ExternalOracle` is the V1 contract and is kept for chains still on it.

### 2.2. POST

| Path | Purpose |
|---|---|
| `/v1/quote` | Price one exact-in leg against supplied leg state |
| `/v1/route` | Rank routes and splits across supplied pool states |
| `/v1/depth` | Build a depth ladder across supplied pool states |
| `/v1/timeseries/bulk` | Several `timeseries` queries in one request |

`GET` on any of these returns `404`: they are POST-only. That 404 is a method mismatch, not a missing endpoint.

### 2.3. WebSocket

`/stream`: live price batches at `wss://data.btr.markets/stream` (market data) and `wss://api.btr.markets/stream` (gateway) where documented. A plain `GET` returns `400` because the request is not an upgrade. The message schema is not documented here; read it off a live connection before depending on it.

---

## 3. The three POST endpoints are stateless pricing kernels

This is the single most important thing to understand about `/v1/quote`, `/v1/route` and `/v1/depth`, and it is not what the endpoint names suggest.

**They hold no pool state.** 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 and hands the numbers back. It is the on-chain pricer, callable off chain, on state you supply.

That means:

- Reading pool state is **your** job, on chain. `/v1/venues` gives you the addresses; the SDK's storage readers (`@btr-protocol/sdk/pool`) give you the slots.
- The kernel is authoritative for arithmetic, not for freshness. Stale inputs produce a confidently wrong number.
- 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:

| Field | Type | Notes |
|---|---|---|
| `curve` | object | `{ header: string, segs: SegWire[], m: number }`; `SegWire` = `{c0,c1,c2,c3,c4,s}`, all `0x` hex |
| `min_dispersion_pbps` | u32 | |
| `vega_bps` | u16 | |
| `min_fee_pbps` | u32 | |
| `kappa_cov_bps` | u16 | |
| `amount_in` | `0x` hex | |
| `reserves` | `0x` hex u128 | native raw units of the token |
| `liabilities` | `0x` hex u128 | native raw units |
| `mark` | `0x` hex U256 | WAD (1e18) |
| `sigma_pbps` | u32 | |
| `selling` | bool | |
| `counterparty` | object | `{reserves, liabilities, vega_bps, kappa_cov_bps}` — the swap's OTHER endpoint; see below |
| `confidence_bps` | u16 | |
| `stale_excess` | u32 | seconds of staleness beyond TTL |
| `proto_share_pct` | u8 | |

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

```bash
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": 25
  }'
```

#### `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 dial.
- `_covToll(cOut, …)` — the convex coverage toll is charged on whichever endpoint the swap **delivers**. On a spoke→base sell that endpoint is the **hub**, and the hub is not exempt: every listed asset including the base runs `0 < κ ≤ BPS`.

So `counterparty` is the endpoint on the far side of your leg — the pool's base on a direct spoke↔base swap, in that token's native raw units. It is **required**. There is no default, because the only available default (a zero endpoint) silently drops the hub's coverage toll and quotes a price the pool will not fill; on the live fleet that gap reaches hundreds of bps at size. A body without it is a `400`.

One case takes a zero endpoint on purpose: a hop whose far token is **interior** to a longer path — the first leg of a spoke→base→spoke cross, whose base is not an endpoint of the swap at all. Send `{"reserves":"0x0","liabilities":"0x0","vega_bps":0,"kappa_cov_bps":0}` there; the chain neither tolls that node nor takes its vega.

Response:

```json
{
  "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…"
}
```

There is no `min_out`. Derive your own slippage floor from `amount_out`; see [Basic Operations §7](/docs/5-1-1-basic-operations#7-slippage-protection).

### 3.2. `POST /v1/route`

| Field | Type | Required |
|---|---|---|
| `pools` | `NamedPoolWire[]` | yes |
| `token_in` | string | yes |
| `token_out` | string | yes |
| `amount_in` | `0x` hex U256 | yes |
| `slices` | u32 | no |
| `min_gain_bps` | u64 | no |
| `max_routes` | usize | no |

`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 the router **drops** every leg that delivers your base rather than quote it toll-free — a missing κ or vega each silently removes a charge the chain makes.

> `base_address` and `base_decimals` are individually optional but **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. The same applies per spoke.

`SpokeWire` = `{ token, pricing, reserves, liabilities, mark, sigma_pbps, confidence_bps, stale_excess, proto_share_pct }` plus `address` and `decimals`. `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:

```json
{
  "best_amount_out": "0x…",
  "best_is_split": false,
  "best_parts": [{ "legs": [], "fraction": "0x…", "amount_out": "0x…" }],
  "singles": [{ "legs": [], "amount_in": "0x…", "amount_out": "0x…" }]
}
```

`LegWire` = `{ pool_tag, token_in, token_out, amount_in, amount_out }`. No `minOut` is computed anywhere in the response; it is the caller's to apply per leg.

### 3.3. `POST /v1/depth`

| Field | Type | Required |
|---|---|---|
| `pools` | `NamedPoolWire[]` | yes |
| `from` | string | yes |
| `to` | string | yes |
| `base_reserves` | `0x` hex | no: per-pool `base_reserves` wins when both are present |
| `samples` | u32 | no; sweep resolution per side per pool, default 48 |

Response: `{ mark, mid, bid, ask, step, bids: Row[], asks: Row[], poolCount }`, `Row` = `{ price, size, cum }`. These are JSON floats; this endpoint is for rendering a book, not for settlement arithmetic.

---

## 4. Response shapes worth knowing

### 4.1. `/v1/md/tickers`

```json
[{"ticker":288583527640858624,"mid":291807.65,"bid":291221.02,"ask":292394.28,
  "ci":12367,"confidence":0,"flags":64,"age_ms":77084658,"status":"dead"}]
```

Two traps:

1. **`ticker` is a numeric id, not a symbol.** The id→symbol mapping is not served by this endpoint. Resolve symbols through `/v1/assets/pools` or `/v1/pools`, which speak symbols directly.
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` will price off a corpse.

### 4.2. `/v1/activity`

```json
{"chainId":5042002,"events":[{"id":"0x…:47","kind":"swap","pool":"btr-crypto",
  "payer":"0x…","amountIn":3.6386154,"amountOut":1.99187305,"logIndex":47}]}
```

`id` is `txHash:logIndex`. Paginate backwards with `before`.

### 4.3. `/v1/venues`

Keyed by chain id. Each entry carries `chain_id`, `name`, and a `contracts` map: `ac`, `admin`, `adminImpl`, `deployer`, `faucet`, `flash`, `flashImpl`, `guardian`, `oracle`, `owner`, `poolFactory`, `poolImpl`, `refOracle`, `treasury`. This is the address source of truth for every integration. Do not hardcode addresses.

### 4.4. Floats on the wire

The `GET` surface returns JSON **floats** for prices and amounts (`mid`, `amountIn`, `amountUsd`). They are 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`, version `0.5.0`.

### 5.1. Reachable entrypoints

These subpaths resolve. Nothing outside this table is importable, whatever a file in the source tree appears to export.

| Import | Surface |
|--------|---------|
| `@btr-protocol/sdk` | Re-exports `./pool`, `./router`, `./amm`, `./venues`, `./oracle`, `./eth`, `./types` and the `./utils` helpers |
| `@btr-protocol/sdk/pool` | `getSwapQuote`, `swap`, `deposit`, `withdraw`, `getPoolData`, `getAsset`, `getCoverageRatio`, `getLPBalance`, `defaultDeadline`, `NATIVE_TOKEN`, `NO_DEADLINE`, `DEFAULT_DEADLINE_S`, `POOL_ABI`, storage readers, types |
| `@btr-protocol/sdk/router` | `planToLegs`, `buildSwapCalls`, `buildApprovalCalls`, `buildSwapExecCalls`, `buildDepositCalls`, `buildRedeemCalls`, `refloorLeg`, `totalValue`, `rankDeposit`, `rankRedeem`, `applySlip` |
| `@btr-protocol/sdk/amm` | `quoteExactIn`, `rankSwap`, `poolStateFrom`, `SwapPlan` |
| `@btr-protocol/sdk/abis` | `POOL_ABI`, `ADMIN_ABI`, `FLASH_ABI`, `POOL_FACTORY_ABI`, `ACCESS_CONTROL_ABI`, `EXTERNAL_ORACLE_V4_ABI`, `EXTERNAL_ORACLE_ABI`, `LP_TOKEN_ABI`, `POOL_HOOKS_ABI`. `EXTERNAL_ORACLE_V2_ABI` is a deprecated alias of `EXTERNAL_ORACLE_V4_ABI` |
| `@btr-protocol/sdk/eth` | `Contract`, RPC client helpers |
| `@btr-protocol/sdk/eth/wallets` | Wallet helpers |
| `@btr-protocol/sdk/oracle` | Signed-quote decode / digest / recover / quorum |
| `@btr-protocol/sdk/governance` | Governance helpers |
| `@btr-protocol/sdk/venues` | Static deployment registry |
| `@btr-protocol/sdk/utils` | `applySlip`, `applySlippage`, maths / format / encoding helpers |
| `@btr-protocol/sdk/utils/format` · `/utils/logger` · `/types` | Formatting, logging, shared types |

**Not importable.** `setApiRoot`, `btrFetch`, `fetchAbi` and `fetchVenues` exist in the source tree but are re-exported by no index and reachable through no subpath. Fetch ABIs and venues with plain `fetch` against the paths in §2.1:

```ts
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/router` as well as `@btr-protocol/sdk/utils` and the package root; all three resolve to the same function.

### 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.

```ts
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. `NO_DEADLINE` is `0xffffffffn`, a far-future sentinel that opts out, cheaper in calldata than `type(uint256).max` and semantically identical against the contract's `block.timestamp > deadline` check.

Routing:

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

> The SDK exported `routeAsync` and `quoteExactInAsync` as thin wrappers over `/v1/route` and `/v1/quote`. Both were removed: they had no callers, and neither had ever worked: they serialised camelCase against a service that requires snake_case, and their pool payload was the SDK's `PoolState` rather than the `NamedPoolWire` the endpoint accepts. Build the request body yourself against §3.1 and §3.2. `planToLegs` and `buildSwapCalls` are unaffected and work on any plan of the right shape.

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`.

---

## 6. Related

[Quotes & Routing](/docs/5-2-2-quotes-routing) · [Cookbook](/docs/5-2-3-cookbook) · [Basic Operations](/docs/5-1-1-basic-operations)
