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: 0means the engine has applied the chain tip. Requiretip_lag == 0, or a limit you set, before you size or sign a transaction.tip_lag: nullmeans 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
overallUserSharesare the user's position.Per bin,
userSharesis the user's balance,binSharesis the bin's total shares, andreserveX/reserveYare 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.
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
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.
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.
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_idas an unsigned ID:to_unsigned_bin_id(active_bin_id).
Withdrawing Liquidity
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
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
Get a quote
POST /quotes/v2/quotereturns the best route.POST /quotes/v2/quote/multireturns every route, withbest_route_index.
Both endpoints take the same request body. Send amount_in as a string in the input token's base units.
Some routes pass through venues the swap router can't execute yet. Each route has an executable flag and a reject_reason. The best route by output can be non-executable, so pick a route with executable: true. POST /quotes/v2/quote also returns the best executable route as best_executable.
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.
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/statusand quote only whiletip_lagis0, or within your own limit./readytells 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) fromGET /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 whencleanandcompletearetrueandtipLagis0. That response's bin set andoverallUserSharesare the position atasOfHeight. An empty position returns200withbins: []. Treat any other status as unknown inventory, never as an empty book..../positions/{pool_id}/binscomes from indexed events. Use it for charts, not for inventory.
Migrating from v1
The v1 page documents the legacy endpoints.
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