For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 and the Hiro API docs.

Check that the engine is caught up with the chain:

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.

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.

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.

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

1

Fetch pool bins and the user's current bins

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

2

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.

3

Execute add liquidity

Call the liquidity router contract with the prepared bins. Use the code under 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).


Withdrawing Liquidity

1

Fetch the user's current bins

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.

2

Prepare bins for withdraw liquidity

Choose the percentage of each bin to withdraw.

3

Execute withdraw liquidity

Call the liquidity router contract with the prepared bins. Use the code under Executing Withdraw Liquidity on the v1 page, and fetch get_token_asset_name from /quotes/v2/tokens.


Moving Liquidity

1

Fetch pool bins and the user's current bins

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

2

Prepare bins for move liquidity

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

3

Execute move liquidity

Call the liquidity router contract with the prepared bins. Use the code under Executing Move Liquidity on the v1 page, and fetch get_token_asset_name from /quotes/v2/tokens.


Swapping Tokens

1

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.

2

Get swap parameters

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

3

Execute a swap

Call the swap router contract with the swap parameters. Use the code under Executing a Swap on the v1 page without changes.


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

Contract-call parameters for a route

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

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.

Migrating from v1

The v1 page 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.

Last updated