π οΈHODLMM API Documentation
This page provides endpoints and sample code for integrating with HODLMM.
API Usage and Documents
The BitFlow HODLMM API can be accessed via the below endpoints. Please reach out to the team for an API key. NOTE: keys will not be required during BETA.
const BFF_API_URL = 'https://bff.bitflowapis.finance/api';
const BFF_API_KEY = '<bitflow-assigned-api-key>';
fetch(url, {
method: 'GET', // or 'POST'
headers: {
'Content-Type': 'application/json',
'X-API-Key': BFF_API_KEY
},
})Adding Liquidity
Getting Pool Bins
Before adding liquidity, you need to get the pool's bins. You can do this via the following endpoint:
/quotes/v1/bins/{pool_id}: Returns all bins for a pool
This endpoint is a GET request.
const BFF_API_URL = 'https://<api-endpoint>/quotes/v1/bins';
const BFF_API_KEY = '<bitflow-assigned-api-key>';
const get_pool_bins = async (pool_id) => {
const response = await fetch(BFF_API_URL + `/${pool_id}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', 'X-API-Key': BFF_API_KEY }
});
const data = await response.json();
return data;
};Getting User Position Bins
After getting pool bins, you need to get the user's position bins. You can do this via the following endpoint:
/app/v1/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool
This endpoint is a GET request.
const BFF_API_URL = 'https://<api-endpoint>/app/v1/users';
const BFF_API_KEY = '<bitflow-assigned-api-key>';
const get_user_position_bins = async (user_address, pool_id) => {
const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}/bins`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', 'X-API-Key': BFF_API_KEY }
});
const data = await response.json();
return data;
};Preparing Bins for Add Liquidity
After getting pool bins and user positions, you need to prepare the bins with the amounts you want to add to each bin.
const bins_to_add = [
{
bin_id: 500,
x_amount: 10000000000,
y_amount: 1750000000
}
];
const prepare_bins_for_add = (pool_bins, user_positions, active_bin_id, bins_to_add) => {
const user_position_bins_map = new Map(
(Array.isArray(user_positions?.bins)
? user_positions.bins
: []).map(bin => [bin.bin_id, 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 (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: 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 ?? 0),
user_liquidity: user_position_bins_map.get(add_bin.bin_id)?.user_liquidity || 0,
has_ever_added_to_bin: user_position_bins_map.has(add_bin.bin_id)
};
});
};Executing Add Liquidity
After preparing the bins to add, you can execute the add by calling the liquidity router contract with the necessary parameters.
const {
intCV,
uintCV,
listCV,
tupleCV,
principalCV,
contractPrincipalCV,
createAssetInfo,
makeStandardFungiblePostCondition,
makeStandardNonFungiblePostCondition,
FungibleConditionCode,
NonFungibleConditionCode,
PostConditionMode,
AnchorMode,
makeContractCall,
broadcastTransaction
} = require('@stacks/transactions');
const { StacksMainnet } = require('@stacks/network');
const stacks_network = new StacksMainnet();
const STACKS_PRIVATE_KEY = '<your_stacks_private_key>';
const STACKS_TRANSACTION_FEE = 10000; // Transaction fee in uSTX (1 STX = 1000000)
const LIQUIDITY_ROUTER_CONTRACT = 'SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1';
const PRICE_SCALE_BPS = 1e8;
const FEE_SCALE_BPS = 1e4;
const get_signed_bin_id = (unsigned_bin_id) => {
return unsigned_bin_id - 500;
};
const get_token_asset_name = async (token_contract) => {
const BFF_API_URL = 'https://<api-endpoint>/quotes/v1/tokens';
const response = await fetch(BFF_API_URL, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
const token = data.tokens.find(t => t.contract_address === token_contract);
if (!token) throw new Error(`Token not found: ${token_contract}`);
return token.asset_name;
};
const calculate_min_dlp_for_bin = (bin, pool_fees, slippage_tolerance = 1) => {
const {
is_active_bin,
bin_price,
reserve_x,
reserve_y,
bin_shares,
x_amount,
y_amount
} = bin;
const {
x_protocol_fee,
x_provider_fee,
x_variable_fee,
y_protocol_fee,
y_provider_fee,
y_variable_fee
} = pool_fees;
const minimum_bin_shares = 10000;
const minimum_burnt_shares = 1000;
const y_amount_scaled = y_amount * PRICE_SCALE_BPS;
const reserve_y_scaled = reserve_y * PRICE_SCALE_BPS;
const add_liquidity_value = bin_price * x_amount + y_amount_scaled;
const bin_liquidity_value = bin_price * reserve_x + reserve_y_scaled;
const dlp = bin_shares === 0 || bin_liquidity_value === 0
? Math.sqrt(add_liquidity_value)
: (add_liquidity_value * bin_shares) / bin_liquidity_value;
let x_amount_fees_liquidity = 0;
let y_amount_fees_liquidity = 0;
if (is_active_bin && dlp > 0) {
const x_liquidity_fee = x_protocol_fee + x_provider_fee + x_variable_fee;
const y_liquidity_fee = y_protocol_fee + y_provider_fee + y_variable_fee;
const x_amount_withdrawable = (dlp * (reserve_x + x_amount)) / (bin_shares + dlp);
const y_amount_withdrawable = (dlp * (reserve_y + y_amount)) / (bin_shares + dlp);
if (y_amount_withdrawable > y_amount && x_amount > x_amount_withdrawable) {
const max_x_amount_fees_liquidity = ((x_amount - x_amount_withdrawable) * x_liquidity_fee) / FEE_SCALE_BPS;
x_amount_fees_liquidity = x_amount > max_x_amount_fees_liquidity ? max_x_amount_fees_liquidity : x_amount;
};
if (x_amount_withdrawable > x_amount && y_amount > y_amount_withdrawable) {
const max_y_amount_fees_liquidity = ((y_amount - y_amount_withdrawable) * y_liquidity_fee) / FEE_SCALE_BPS;
y_amount_fees_liquidity = y_amount > max_y_amount_fees_liquidity ? max_y_amount_fees_liquidity : y_amount;
};
};
const x_amount_post_fees = x_amount - x_amount_fees_liquidity;
const y_amount_post_fees = y_amount - y_amount_fees_liquidity;
const y_amount_post_fees_scaled = y_amount_post_fees * PRICE_SCALE_BPS;
const reserve_x_post_fees = reserve_x + x_amount_fees_liquidity;
const reserve_y_post_fees_scaled = (reserve_y + y_amount_fees_liquidity) * PRICE_SCALE_BPS;
const add_liquidity_value_post_fees = bin_price * x_amount_post_fees + y_amount_post_fees_scaled;
const bin_liquidity_value_post_fees = bin_price * reserve_x_post_fees + reserve_y_post_fees_scaled;
let dlp_post_fees;
if (bin_shares === 0) {
const intended_dlp = Math.sqrt(add_liquidity_value_post_fees);
dlp_post_fees = intended_dlp >= minimum_bin_shares ? intended_dlp - minimum_burnt_shares : 0;
} else if (bin_liquidity_value_post_fees === 0) {
dlp_post_fees = Math.sqrt(add_liquidity_value_post_fees);
} else {
dlp_post_fees = (add_liquidity_value_post_fees * bin_shares) / bin_liquidity_value_post_fees;
};
const min_dlp = Math.floor(dlp_post_fees * (1 - slippage_tolerance / 1e2));
return {
min_dlp,
x_amount_fees_liquidity: Math.ceil(x_amount_fees_liquidity),
y_amount_fees_liquidity: Math.ceil(y_amount_fees_liquidity)
};
};
const add_liquidity = async (
pool_contract,
pool_data,
x_token_contract,
y_token_contract,
bins_to_add,
sender_address,
slippage_tolerance = 1
) => {
const router_contract_address = LIQUIDITY_ROUTER_CONTRACT.split('.')[0];
const router_contract_name = LIQUIDITY_ROUTER_CONTRACT.split('.')[1];
const pool_contract_address = pool_contract.split('.')[0];
const pool_contract_name = pool_contract.split('.')[1];
const x_token_contract_address = x_token_contract.split('.')[0];
const x_token_contract_name = x_token_contract.split('.')[1];
const token_x_asset_name = await get_token_asset_name(x_token_contract);
const token_x_asset_info = createAssetInfo(x_token_contract_address, x_token_contract_name, token_x_asset_name);
const y_token_contract_address = y_token_contract.split('.')[0];
const y_token_contract_name = y_token_contract.split('.')[1];
const token_y_asset_name = await get_token_asset_name(y_token_contract);
const token_y_asset_info = createAssetInfo(y_token_contract_address, y_token_contract_name, token_y_asset_name);
const pool_fees = {
x_protocol_fee: pool_data.x_protocol_fee || 0,
x_provider_fee: pool_data.x_provider_fee || 0,
x_variable_fee: pool_data.x_variable_fee || 0,
y_protocol_fee: pool_data.y_protocol_fee || 0,
y_provider_fee: pool_data.y_provider_fee || 0,
y_variable_fee: pool_data.y_variable_fee || 0
};
const bin_add_positions = bins_to_add.map(bin => {
const { min_dlp, x_amount_fees_liquidity, y_amount_fees_liquidity } = calculate_min_dlp_for_bin(bin, pool_fees, slippage_tolerance);
const max_x_liquidity_fee = Math.ceil(x_amount_fees_liquidity * (1 + slippage_tolerance / 1e2));
const max_y_liquidity_fee = Math.ceil(y_amount_fees_liquidity * (1 + slippage_tolerance / 1e2));
return tupleCV({
'pool-trait': contractPrincipalCV(pool_contract_address, pool_contract_name),
'x-token-trait': contractPrincipalCV(x_token_contract_address, x_token_contract_name),
'y-token-trait': contractPrincipalCV(y_token_contract_address, y_token_contract_name),
'bin-id': intCV(get_signed_bin_id(bin.bin_id)),
'x-amount': uintCV(bin.x_amount),
'y-amount': uintCV(bin.y_amount),
'min-dlp': uintCV(min_dlp),
'max-x-liquidity-fee': uintCV(max_x_liquidity_fee),
'max-y-liquidity-fee': uintCV(max_y_liquidity_fee)
});
});
const total_x_amount = bins_to_add.reduce((sum, bin) => sum + bin.x_amount, 0);
const total_y_amount = bins_to_add.reduce((sum, bin) => sum + bin.y_amount, 0);
const post_conditions = [];
post_conditions.push(
makeStandardFungiblePostCondition(
sender_address,
FungibleConditionCode.LessEqual,
total_x_amount.toString(),
token_x_asset_info
)
);
post_conditions.push(
makeStandardFungiblePostCondition(
sender_address,
FungibleConditionCode.LessEqual,
total_y_amount.toString(),
token_y_asset_info
)
);
bins_to_add.forEach(bin => {
if (bin.has_ever_added_to_bin) {
post_conditions.push(
makeStandardNonFungiblePostCondition(
sender_address,
NonFungibleConditionCode.Sends,
createAssetInfo(pool_contract_address, pool_contract_name, 'pool-token-id'),
tupleCV({
'token-id': uintCV(bin.bin_id),
'owner': principalCV(sender_address)
})
)
);
};
});
const tx_options = {
contractAddress: router_contract_address,
contractName: router_contract_name,
functionName: 'add-liquidity-multi',
functionArgs: [listCV(bin_add_positions)],
senderKey: STACKS_PRIVATE_KEY,
network: stacks_network,
fee: STACKS_TRANSACTION_FEE,
postConditions: post_conditions,
postConditionMode: PostConditionMode.Deny,
anchorMode: AnchorMode.Any
};
const transaction = await makeContractCall(tx_options);
const response = await broadcastTransaction(transaction, stacks_network);
return response;
};Withdrawing Liquidity
Moving Liquidity
Swapping Tokens
Getting Data
Getting Available Tokens
You can get all available tokens via the following endpoint:
/quotes/v1/tokens: Returns all available tokens
This endpoint is a GET request.
Getting Available Pools
You can get all available pools via the following endpoint:
/quotes/v1/pools: Returns all available pools
This endpoint is a GET request.
Getting Available Pairs
You can get all available trading pairs via the following endpoint:
/quotes/v1/pairs: Returns all available trading pairs
This endpoint is a GET request.
Getting Pool Data
You can get all data for a pool via the following endpoint:
quotes/v1/pools/{pool_id}: Returns all data for a pool
This endpoint is a GET request.
Getting Pool Bins
You can get all bins for a pool via the following endpoint:
/quotes/v1/bins/{pool_id}: Returns all bins for a pool
This endpoint is a GET request.
Getting Pool Bin Price History
You can get the bin price history for a pool via the following endpoint:
/app/v1/pools/{pool_id}/bin-price-history: Returns bin price history for a pool
This endpoint is a GET request.
Getting User Position for a Pool
You can get the position for a user for a pool via the following endpoint:
/app/v1/users/{user_address}/positions/{pool_id}: Returns position for a user for a pool
This endpoint is a GET request.
Getting User Position Bins for a Pool
You can get the position bins for a user for a pool via the following endpoint:
/app/v1/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool
This endpoint is a GET request.
Last updated