> For the complete documentation index, see [llms.txt](https://docs.bitflow.finance/bitflow-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation-v2.md).

# HODLMM API Documentation (v2)

Engine v2 endpoints and sample code for HODLMM retail integrators and market makers.

#### Getting started

**Base URL:** `https://bff.bitflowapis.finance/api`

The API is public. You can call it without a key at up to **100 requests per second** per IP. For a higher limit, ask in **Intercom** and we will issue an `X-API-Key` for your integration.

This API does not include a Stacks node or the Stacks API. For Stacks RPC, use [Hiro Platform](https://platform.hiro.so/) and the [Hiro API docs](https://docs.hiro.so/).

Check that the engine is caught up with the chain:

```bash
curl -sS https://bff.bitflowapis.finance/api/quotes/v2/status \
  | jq '{ready, state_height, chain_tip, tip_lag, pool_count}'
```

**Shared helpers**

The samples on this page use these helpers.

```javascript
const BFF_API_URL = 'https://bff.bitflowapis.finance/api';
const BFF_API_KEY = null; // Optional. Set to your X-API-Key for a raised rate limit.

const bff = async (path, { method = 'GET', body } = {}) => {
  const headers = { 'Content-Type': 'application/json' };
  if (BFF_API_KEY) headers['X-API-Key'] = BFF_API_KEY;

  const response = await fetch(`${BFF_API_URL}${path}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined
  });

  if (!response.ok) throw new Error(`${method} ${path} returned ${response.status}`);
  return response.json();
};

// Quote v2 and App v2 return signed bin IDs (-500 to 500).
// Contract calls take unsigned bin IDs (0 to 1000).
const to_unsigned_bin_id = (signed_bin_id) => signed_bin_id + 500;

// Every bin in the pool, with price, reserves, and total shares (`liquidity`).
const get_pool_bins = async (pool_id) => {
  const data = await bff(`/quotes/v2/bins/${pool_id}`);
  if (!data.success) throw new Error(data.error ?? `No bins for ${pool_id}`);
  return data;
};

// Position at asOfHeight: overallUserShares plus per-bin userShares, binShares, reserveX, reserveY.
// Size an add, withdraw, or move only when clean, complete, and tipLag are good. See "Your position".
const get_user_current_bins = (user_address, pool_id) =>
  bff(`/app/v2/users/${user_address}/positions/${pool_id}/current-bins`);
```

#### Core concepts

**Pool IDs**

Each pool has a contract principal, such as `SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-sbtc-usdcx-v-1-bps-10`. `GET /quotes/v2/pools` returns it as `pool_id`. Prefer the contract principal in every call. Quote v2 also accepts short aliases such as `dlmm_1`. `GET /app/v2/users/{user}/liquidity/{pool_id}` resolves aliases and case-insensitive pool ids. Other App v2 routes (for example `/app/v2/pools/{pool_id}` and `/positions/{pool_id}/bins`) may still answer `not found` for an alias — prefer the principal.

For an unknown pool, `GET /quotes/v2/bins/{pool_id}` returns HTTP `200` with `success: false` and an `error` message. Check `success` as well as the status code.

**Bin IDs**

Quote v2 and App v2 return **signed** bin IDs, from `-500` to `500`. That includes `active_bin_id` and the `bin_id` of each bin.

Two places take **unsigned** IDs, from `0` to `1000`:

* `GET /quotes/v2/bins/{pool_id}/{bin_id}` (single-bin lookup)
* Contract calls

Convert with `unsigned = signed + 500`. The prepare functions on this page take signed IDs and return unsigned ones, so you can pass their output to the executing code without changes.

**Data freshness**

Quote v2 serves pool state from memory at the latest block it has applied. `GET /quotes/v2/status` reports how far that state trails the chain:

* `tip_lag: 0` means the engine has applied the chain tip. Require `tip_lag == 0`, or a limit you set, before you size or sign a transaction.
* `tip_lag: null` means the engine does not know the chain tip. Treat it as behind.

App v2 builds pool stats, price history, and position summaries from indexed chain events, so those reads can trail the tip. `GET /app/v2/ready` reports `ready` and `logTailLag`. Position sizing uses `current-bins`, which reports its own `tipLag`, `clean`, and `complete`.

**Your position**

Size an add, withdraw, or move from `GET /app/v2/users/{address}/positions/{pool_id}/current-bins` only when `clean` is `true`, `complete` is `true`, and `tipLag` is `0`.

When those three hold, the response is the position at `asOfHeight`:

* The bin set and `overallUserShares` are the user's position.
* Per bin, `userShares` is the user's balance, `binShares` is the bin's total shares, and `reserveX` / `reserveY` are that bin's reserves.
* Share amounts are decimal strings.

An empty position is HTTP `200` with `bins: []`. Any other status means the position is unknown. Do not treat it as an empty book.

`GET /app/v2/users/{address}/positions/{pool_id}/bins` is the indexed view for charts and display. Do not size a transaction from it.

**Current Balance (`/liquidity`)**

The app **Current Balance** card uses `GET /app/v2/users/{address}/liquidity/{pool_id}`. For DLMM pools it re-values on read from held-bin reserves × the user's shares ÷ **absolute bin share supply** (including burn-address / unindexed supply printed on `BinUpdate`). That matches Clarity `dlmm_position_amounts` and `current-bins`. Do not treat ownership as `user_shares / sum(indexed holders)`.

Wing / out-of-range positions keep per-bin X/Y splits. Pool-wide pro-rata applies only when the user has no held-bin rows. Per-bin USD on `…/positions/…/bins` uses the same absolute bin supply. Still size transactions from `current-bins` only. See Current Balance and `/liquidity` valuation.

{% hint style="info" %}
Absolute `bin_shares` require derived checkpoint **v6** on the serving host. Until that cutover finishes on a given environment, `/liquidity` on that host can still disagree with chain.
{% endhint %}

**Strict vs Simple mode**

**Strict mode** uses exact bin IDs. Use it for precise liquidity operations. You can broadcast these transactions in Deny mode. If on-chain state changes in a way that affects your bins or amounts, the transaction reverts. For example, an add to the active bin reverts if the active bin moves by one before execution.

**Simple mode** uses offsets from the active bin instead of absolute bin IDs. Broadcast these transactions in Allow mode. The contract applies the operation relative to the active bin at execution time, so the transaction can still confirm after the active bin moves.

#### Adding Liquidity

{% stepper %}
{% step %}
**Fetch pool bins and the user's current bins**

```javascript
const pool_bins = await get_pool_bins(pool_id);
const active_bin_id = pool_bins.active_bin_id; // signed
const user_bins = await get_user_current_bins(user_address, pool_id);
```

Require `clean`, `complete`, and `tipLag == 0` before you size. The bin set and `overallUserShares` are the position at `asOfHeight`.
{% endstep %}

{% step %}
**Prepare bins for add liquidity**

Choose the bins and the amount of each token to add. Below the active bin you can add only token Y. Above it you can add only token X.

{% hint style="warning" %}
`liquidity` on a Quote v2 bin holds the bin's total shares as a string, and it can be `null`. The samples stop on `null`. Treating it as `0` sends the executing code down the first-deposit path and produces a wrong `min_dlp`.
{% endhint %}

{% tabs %}
{% tab title="Strict mode" %}

```javascript
// Signed bin IDs
const bins_to_add = [
  { bin_id: active_bin_id, x_amount: 10000000000, y_amount: 1750000000 },
  { bin_id: active_bin_id - 1, x_amount: 0, y_amount: 1750000000 }
];

const prepare_bins_for_add = (pool_bins, user_bins, active_bin_id, bins_to_add) => {
  const user_bins_map = new Map((user_bins?.bins ?? []).map(bin => [bin.binIdSigned, bin]));

  return bins_to_add.map(add_bin => {
    const pool_bin = pool_bins.bins.find(b => b.bin_id === add_bin.bin_id);

    if (!pool_bin) throw new Error(`Bin ${add_bin.bin_id} not found in pool`);
    if (pool_bin.liquidity === null) throw new Error(`Bin ${add_bin.bin_id} has no share data yet`);

    if (add_bin.bin_id < active_bin_id && add_bin.x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    }

    if (add_bin.bin_id > active_bin_id && add_bin.y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    }

    if (add_bin.bin_id === active_bin_id && add_bin.x_amount === 0 && add_bin.y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    }

    return {
      is_active_bin: add_bin.bin_id === active_bin_id,
      bin_id: to_unsigned_bin_id(add_bin.bin_id),
      x_amount: add_bin.x_amount,
      y_amount: add_bin.y_amount,
      bin_price: Number(pool_bin.price),
      reserve_x: Number(pool_bin.reserve_x),
      reserve_y: Number(pool_bin.reserve_y),
      bin_shares: Number(pool_bin.liquidity),
      user_liquidity: Number(user_bins_map.get(add_bin.bin_id)?.userShares ?? 0),
      has_ever_added_to_bin: user_bins_map.has(add_bin.bin_id)
    };
  });
};
```

{% endtab %}

{% tab title="Simple mode" %}

```javascript
const bins_to_add_relative = [
  { active_bin_offset: 0, x_amount: 10000000000, y_amount: 1750000000 },
  { active_bin_offset: -1, x_amount: 0, y_amount: 1750000000 }
];

const prepare_bins_for_relative_add = (pool_bins, user_bins, active_bin_id, bins_to_add) => {
  const user_bins_map = new Map((user_bins?.bins ?? []).map(bin => [bin.binIdSigned, bin]));

  return bins_to_add.map(add_bin => {
    const bin_id = active_bin_id + add_bin.active_bin_offset; // signed
    const pool_bin = pool_bins.bins.find(b => b.bin_id === bin_id);

    if (!pool_bin) throw new Error(`Bin ${bin_id} not found in pool`);
    if (pool_bin.liquidity === null) throw new Error(`Bin ${bin_id} has no share data yet`);

    if (bin_id < active_bin_id && add_bin.x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    }

    if (bin_id > active_bin_id && add_bin.y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    }

    if (bin_id === active_bin_id && add_bin.x_amount === 0 && add_bin.y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    }

    return {
      is_active_bin: bin_id === active_bin_id,
      active_bin_offset: add_bin.active_bin_offset,
      bin_id: to_unsigned_bin_id(bin_id),
      x_amount: add_bin.x_amount,
      y_amount: add_bin.y_amount,
      bin_price: Number(pool_bin.price),
      reserve_x: Number(pool_bin.reserve_x),
      reserve_y: Number(pool_bin.reserve_y),
      bin_shares: Number(pool_bin.liquidity),
      user_liquidity: Number(user_bins_map.get(bin_id)?.userShares ?? 0),
      has_ever_added_to_bin: user_bins_map.has(bin_id)
    };
  });
};
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**Execute add liquidity**

Call the liquidity router contract with the prepared bins. Use the code under [Executing Add Liquidity](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation#executing-add-liquidity) on the v1 page, with two changes:

* In `get_token_asset_name`, fetch from `/quotes/v2/tokens`.
* In Simple mode, pass `expected_bin_id` as an unsigned ID: `to_unsigned_bin_id(active_bin_id)`.
  {% endstep %}
  {% endstepper %}

***

#### Withdrawing Liquidity

{% stepper %}
{% step %}
**Fetch the user's current bins**

```javascript
const user_bins = await get_user_current_bins(user_address, pool_id);
const active_bin_id = user_bins.activeBinIdSigned;
```

Require `clean`, `complete`, and `tipLag == 0` before you size. The bin set and `overallUserShares` are the position at `asOfHeight`. Per bin, use `userShares`, `binShares`, `reserveX`, and `reserveY`.
{% endstep %}

{% step %}
**Prepare bins for withdraw liquidity**

Choose the percentage of each bin to withdraw.

{% tabs %}
{% tab title="Strict mode" %}

```javascript
const prepare_bins_for_withdraw = (user_bins, withdrawal_percentage) => {
  return user_bins.bins
    .filter(bin => Number(bin.userShares) > 0)
    .map(bin => ({
      bin_id: bin.binIdUnsigned,
      user_liquidity: Number(bin.userShares),
      liquidity: Number(bin.binShares),
      reserve_x: Number(bin.reserveX),
      reserve_y: Number(bin.reserveY),
      withdrawal_percentage,
      has_ever_added_to_bin: true
    }));
};
```

{% endtab %}

{% tab title="Simple mode" %}

```javascript
const prepare_bins_for_relative_withdraw = (user_bins, active_bin_id, withdrawal_percentage) => {
  return user_bins.bins
    .filter(bin => Number(bin.userShares) > 0)
    .map(bin => ({
      active_bin_offset: bin.binIdSigned - active_bin_id,
      bin_id: bin.binIdUnsigned,
      user_liquidity: Number(bin.userShares),
      liquidity: Number(bin.binShares),
      reserve_x: Number(bin.reserveX),
      reserve_y: Number(bin.reserveY),
      withdrawal_percentage,
      has_ever_added_to_bin: true
    }));
};
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**Execute withdraw liquidity**

Call the liquidity router contract with the prepared bins. Use the code under [Executing Withdraw Liquidity](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation#executing-withdraw-liquidity) on the v1 page, and fetch `get_token_asset_name` from `/quotes/v2/tokens`.
{% endstep %}
{% endstepper %}

***

#### Moving Liquidity

{% stepper %}
{% step %}
**Fetch pool bins and the user's current bins**

```javascript
const pool_bins = await get_pool_bins(pool_id);
const active_bin_id = pool_bins.active_bin_id; // signed
const user_bins = await get_user_current_bins(user_address, pool_id);
```

Require `clean`, `complete`, and `tipLag == 0` before you size. The bin set and `overallUserShares` are the position at `asOfHeight`.
{% endstep %}

{% step %}
**Prepare bins for move liquidity**

Choose the bin to move from, the bin to move to, and the number of shares to move.

{% tabs %}
{% tab title="Strict mode" %}

```javascript
// Signed bin IDs. `amount` is a share count.
const bins_to_move = [
  { from_bin_id: -20, to_bin_id: -2, amount: 12848399 }
];

const prepare_bins_for_move = (pool_bins, user_bins, active_bin_id, bins_to_move) => {
  const user_bins_map = new Map((user_bins?.bins ?? []).map(bin => [bin.binIdSigned, bin]));

  return bins_to_move.map(move => {
    const { from_bin_id, to_bin_id, amount } = move;

    if (from_bin_id === to_bin_id) throw new Error('Cannot move liquidity to the same bin');

    const from_bin = user_bins_map.get(from_bin_id);
    if (!from_bin) throw new Error('Unable to find from_bin in user positions');

    const user_shares = Number(from_bin.userShares);
    if (user_shares === 0) throw new Error('User has no liquidity in from_bin');
    if (amount > user_shares) throw new Error('Amount exceeds user liquidity in from_bin');

    const to_bin = pool_bins.bins.find(b => b.bin_id === to_bin_id);
    if (!to_bin) throw new Error('Unable to find to_bin in pool bins');
    if (to_bin.liquidity === null) throw new Error(`Bin ${to_bin_id} has no share data yet`);

    const x_amount = Math.floor((amount * Number(from_bin.reserveX)) / Number(from_bin.binShares));
    const y_amount = Math.floor((amount * Number(from_bin.reserveY)) / Number(from_bin.binShares));

    if (to_bin_id < active_bin_id && x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    }

    if (to_bin_id > active_bin_id && y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    }

    if (to_bin_id === active_bin_id && x_amount === 0 && y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    }

    return {
      from_bin_id: to_unsigned_bin_id(from_bin_id),
      to_bin_id: to_unsigned_bin_id(to_bin_id),
      amount,
      x_amount,
      y_amount,
      bin_price: Number(to_bin.price),
      reserve_x: Number(to_bin.reserve_x),
      reserve_y: Number(to_bin.reserve_y),
      bin_shares: Number(to_bin.liquidity),
      is_active_bin: to_bin_id === active_bin_id,
      has_ever_added_to_from_bin: true,
      has_ever_added_to_to_bin: user_bins_map.has(to_bin_id)
    };
  });
};
```

{% endtab %}

{% tab title="Simple mode" %}

```javascript
// Signed bin IDs. `amount` is a share count.
const bins_to_move_relative = [
  { from_bin_id: -20, active_bin_offset: -2, amount: 12848399 }
];

const prepare_bins_for_relative_move = (pool_bins, user_bins, active_bin_id, bins_to_move) => {
  const user_bins_map = new Map((user_bins?.bins ?? []).map(bin => [bin.binIdSigned, bin]));

  return bins_to_move.map(move => {
    const { from_bin_id, active_bin_offset, amount } = move;
    const to_bin_id = active_bin_id + active_bin_offset; // signed

    if (from_bin_id === to_bin_id) throw new Error('Cannot move liquidity to the same bin');

    const from_bin = user_bins_map.get(from_bin_id);
    if (!from_bin) throw new Error('Unable to find from_bin in user positions');

    const user_shares = Number(from_bin.userShares);
    if (user_shares === 0) throw new Error('User has no liquidity in from_bin');
    if (amount > user_shares) throw new Error('Amount exceeds user liquidity in from_bin');

    const to_bin = pool_bins.bins.find(b => b.bin_id === to_bin_id);
    if (!to_bin) throw new Error('Unable to find to_bin in pool bins');
    if (to_bin.liquidity === null) throw new Error(`Bin ${to_bin_id} has no share data yet`);

    const x_amount = Math.floor((amount * Number(from_bin.reserveX)) / Number(from_bin.binShares));
    const y_amount = Math.floor((amount * Number(from_bin.reserveY)) / Number(from_bin.binShares));

    if (to_bin_id < active_bin_id && x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    }

    if (to_bin_id > active_bin_id && y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    }

    if (to_bin_id === active_bin_id && x_amount === 0 && y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    }

    return {
      from_bin_id: to_unsigned_bin_id(from_bin_id),
      active_bin_offset,
      to_bin_id: to_unsigned_bin_id(to_bin_id),
      amount,
      x_amount,
      y_amount,
      bin_price: Number(to_bin.price),
      reserve_x: Number(to_bin.reserve_x),
      reserve_y: Number(to_bin.reserve_y),
      bin_shares: Number(to_bin.liquidity),
      is_active_bin: to_bin_id === active_bin_id,
      has_ever_added_to_from_bin: true,
      has_ever_added_to_to_bin: user_bins_map.has(to_bin_id)
    };
  });
};
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**Execute move liquidity**

Call the liquidity router contract with the prepared bins. Use the code under [Executing Move Liquidity](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation#executing-move-liquidity) on the v1 page, and fetch `get_token_asset_name` from `/quotes/v2/tokens`.
{% endstep %}
{% endstepper %}

***

#### Swapping Tokens

{% stepper %}
{% step %}
**Get a quote**

* `POST /quotes/v2/quote` returns the best route.
* `POST /quotes/v2/quote/multi` returns every route, with `best_route_index`.

Both endpoints take the same request body. Send `amount_in` as a string in the input token's base units.

```javascript
const get_quote = (input_token, output_token, amount_in, amm_strategy = 'best', slippage_tolerance = 1) =>
  bff('/quotes/v2/quote/multi', {
    method: 'POST',
    body: {
      input_token, // Input token contract principal
      output_token, // Output token contract principal
      amount_in: String(amount_in), // Base units, as a string
      amm_strategy, // Optional, default 'best'
      slippage_tolerance // Optional, 1 = 1%
    }
  });
```

{% hint style="warning" %}
Some routes pass through venues the **legacy** `/quotes/v2/swap` router can't execute yet. Each route has an `executable` flag and a `reject_reason`. For `/quotes/v2/swap`, pick `executable: true` (or use `best_executable` from `POST /quotes/v2/quote`). For `/quotes/v2/agg/swap`, prefer `agg_buildable: true` and do **not** gate on `executable` — external venues are often non-executable on the legacy path but still buildable on the aggregator.
{% endhint %}

```javascript
const quote = await get_quote(input_token, output_token, amount_in);
const route = quote.routes.find(r => r.executable);
if (!route) throw new Error('No executable route');
```

{% endstep %}

{% step %}
**Get swap parameters**

`POST /quotes/v2/swap` turns a route into contract-call parameters.

```javascript
const get_swap_params = (amount_in, input_token, output_token, route) =>
  bff('/quotes/v2/swap', {
    method: 'POST',
    body: {
      execution_path: route.execution_path,
      amount_in: String(amount_in),
      amount_out: route.amount_out,
      input_token,
      output_token,
      input_token_decimals: route.input_token_decimals,
      output_token_decimals: route.output_token_decimals,
      slippage_tolerance: route.slippage_tolerance,
      swap_parameters_type: 'simple', // Optional: 'simple' (default) or 'full'
      provider: 'SP3F7GQ48JY59521DZEE6KABHBF4Q33PEYJ823ZXQ' // Optional referral / attribution principal
    }
  });
```

{% endstep %}

{% step %}
**Get aggregator swap parameters (multi-venue)**

`POST /quotes/v2/agg/swap` builds contract-call args for Bitflow `agg-*` routers. Collapse consecutive identical `pool_id`s from `execution_path` into `hops` (max 4). Prefer routes with `agg_buildable: true`. Do **not** gate on `executable` — that flag is for legacy `/quotes/v2/swap` only.

```javascript
// hops: [{ pool_id, token_in, token_out }, ...] from route_path + collapsed execution_path
const get_agg_swap_params = (amount_in, route, hops, recipient, provider = null) =>
  bff('/quotes/v2/agg/swap', {
    method: 'POST',
    body: {
      hops,
      amount_in: String(amount_in),
      amount_out: route.amount_out,
      slippage_tolerance: route.slippage_tolerance,
      recipient, // principal that receives the output
      snapshot_height: route.snapshot_height, // from the quote
      sender: recipient, // optional; used for post-conditions
      provider // optional referral principal, or omit / null
    }
  });
```

The response includes `contract_address`, `contract_name`, `function_name`, `function_args`, `function_args_hex`, and deny-mode `post_conditions`. When `provider` is set, the last Clarity arg is `(some <provider>)`; when omitted, it is `none`.
{% endstep %}

{% step %}
**Execute a swap**

Call the swap router contract with the swap parameters. Use the code under [Executing a Swap](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation#executing-a-swap) on the v1 page without changes.
{% endstep %}
{% endstepper %}

***

#### Referral attribution (`provider`)

Both Quote v2 swap builders accept an optional `provider` Stacks principal. That address is embedded in the on-chain call so Bitflow can attribute the swap to your integration.

| Endpoint                   | Where `provider` lands                                                                                     |
| -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `POST /quotes/v2/swap`     | Inside the Clarity parameters tuple as `provider` (`none` when omitted)                                    |
| `POST /quotes/v2/agg/swap` | Trailing Clarity `(optional principal)` on every Bitflow `agg-*` / `swap-*` function (`none` when omitted) |

{% hint style="info" %}
Bitflow does **not** currently run automated revenue sharing from `provider` attribution. If you are a partner and want to discuss referral economics, contact the team (<help@bitflow.finance> or Discord) — we are open to partner-specific arrangements.
{% endhint %}

Pass your provider address in the request body (example: `SP3F7GQ48JY59521DZEE6KABHBF4Q33PEYJ823ZXQ`). Omit the field or send `null` to leave attribution empty.

On Bitflow aggregator routers (`agg-*`), `provider` is print-only: it does not change routing, amounts, or fees. Prefer `/quotes/v2/agg/swap` for multi-venue / aggregator execution; keep `/quotes/v2/swap` for legacy Bitflow-router routes where `executable: true`.

***

#### Endpoint Reference

All paths are relative to `https://bff.bitflowapis.finance/api`.

| Method | Path                                                    | Returns                                                                            |
| ------ | ------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| GET    | `/quotes/v2/status`                                     | Engine height, chain tip, and `tip_lag`                                            |
| GET    | `/quotes/v2/ready`                                      | `200` when Quote v2 is serving                                                     |
| GET    | `/quotes/v2/tokens`                                     | Supported tokens                                                                   |
| GET    | `/quotes/v2/pools`                                      | All pools, with contract `pool_id`                                                 |
| GET    | `/quotes/v2/pools/{pool_id}`                            | One pool: active bin, bin step, fees, status                                       |
| GET    | `/quotes/v2/pairs?input_token={token}`                  | Tokens you can swap `input_token` into                                             |
| GET    | `/quotes/v2/bins/{pool_id}`                             | All bins (signed IDs) with price, reserves, `liquidity`                            |
| GET    | `/quotes/v2/bins/{pool_id}/active`                      | Active bin ID (signed)                                                             |
| GET    | `/quotes/v2/bins/{pool_id}/{bin_id}`                    | One bin, looked up by **unsigned** ID                                              |
| POST   | `/quotes/v2/quote`                                      | Best route, plus `best_executable`                                                 |
| POST   | `/quotes/v2/quote/multi`                                | All routes                                                                         |
| POST   | `/quotes/v2/swap`                                       | Legacy router contract-call parameters (optional `provider`)                       |
| POST   | `/quotes/v2/agg/swap`                                   | Aggregator (`agg-*`) contract-call parameters (optional `provider`)                |
| GET    | `/app/v2/ready`                                         | App v2 readiness and `logTailLag`                                                  |
| GET    | `/app/v2/pools/{pool_id}`                               | Pool details and analytics                                                         |
| GET    | `/app/v2/pools/{pool_id}/bin-price-history`             | Active bin and price history                                                       |
| GET    | `/app/v2/users/{user}/liquidity/{pool_id}`              | Current Balance: token amounts/value from absolute bin supply (aliases OK)         |
| GET    | `/app/v2/users/{user}/positions/{pool_id}/current-bins` | Position at `asOfHeight`. Size only when `clean`, `complete`, and `tipLag` are `0` |
| GET    | `/app/v2/users/{user}/positions/{pool_id}/bins`         | Position bins from indexed events, for charts and display                          |

```javascript
const pool = await bff(`/quotes/v2/pools/${pool_id}`);
```

#### Market Makers

* Poll `GET /quotes/v2/status` and quote only while `tip_lag` is `0`, or within your own limit. `/ready` tells you the service is up. It does not tell you the engine has applied the chain tip.
* Read the ladder from `GET /quotes/v2/bins/{pool_id}` and pool state (active bin, bin step, fees) from `GET /quotes/v2/pools/{pool_id}`. Size off price, reserves, and your own inventory.
* Read inventory from `GET /app/v2/users/{address}/positions/{pool_id}/current-bins`. Size only when `clean` and `complete` are `true` and `tipLag` is `0`. That response's bin set and `overallUserShares` are the position at `asOfHeight`. An empty position returns `200` with `bins: []`. Treat any other status as unknown inventory, never as an empty book.
* `.../positions/{pool_id}/bins` comes from indexed events. Use it for charts, not for inventory.

```bash
BASE=https://bff.bitflowapis.finance/api
POOL=SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-sbtc-usdcx-v-1-bps-10

curl -s "$BASE/quotes/v2/pools/$POOL" | jq '{pool_id, active_bin, bin_step, active, quotable}'

curl -s "$BASE/quotes/v2/bins/$POOL" | jq '{active_bin_id, bin_count: (.bins | length)}'

curl -s "$BASE/app/v2/users/<your-address>/positions/$POOL/current-bins" \
  | jq '{clean, complete, tipLag, activeBinIdSigned, bins: (.bins | length)}'
```

#### Migrating from v1

The [v1 page](https://docs.bitflow.finance/bitflow-documentation/developers/hodlmm-api-documentation) documents the legacy endpoints.

| Area           | v1                                       | v2                                                                             |
| -------------- | ---------------------------------------- | ------------------------------------------------------------------------------ |
| Paths          | `/quotes/v1`, `/app/v1`                  | `/quotes/v2`, `/app/v2`                                                        |
| Pool IDs       | Short aliases such as `dlmm_3`           | Contract principal. `/liquidity` resolves aliases; prefer principal elsewhere. |
| Bin IDs        | Unsigned, `0` to `1000`                  | Signed, `-500` to `500`. Single-bin lookup still takes unsigned.               |
| Freshness      | `X-Allow-Fallback`, DB to chain fallback | No fallback. Check `tip_lag` on `/quotes/v2/status`.                           |
| Position bins  | `/app/v1/.../bins`                       | `/app/v2/.../current-bins` for transactions                                    |
| Contract calls | v1 executing code                        | Same code, with unsigned bin IDs and `/quotes/v2/tokens`                       |

Move Quote and App calls to v2 together. A client that mixes v1 pool IDs with v2 bins can get HTTP `200` with empty bins and `Pool not found`.
