πŸ› οΈ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

1

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;
};
2

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;
};
3

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)
    };
  });
};
4

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

1

Getting User Position Bins

Before withdrawing liquidity, 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.

2

Preparing Bins for Withdraw Liquidity

After getting user positions, you need to prepare the bins with the percentage you want to withdraw from each bin.

3

Executing Withdraw Liquidity

After preparing the bins to withdraw, you can execute the withdrawal by calling the liquidity router contract with the necessary parameters.


Moving Liquidity

1

Getting Pool Bins

Before moving 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.

2

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.

3

Preparing Bins for Move Liquidity

After getting pool bins and user positions, you need to prepare the bins you want to move liquidity from, to, and the amount of liquidity to move.

4

Executing Move Liquidity

After preparing the bins to move, you can execute the move by calling the liquidity router contract with the necessary parameters.


Swapping Tokens

1

Getting a Quote

Before executing a swap, you need to get a quote. You can do this via the following endpoints:

  • /quotes/v1/quote: Returns the best route

  • /quotes/v1/quote/multi: Returns all routes

These endpoints are POST requests and the request body is the same for both.

2

Getting Swap Parameters

After getting a quote, you need to generate the swap parameters. You can do this via the following endpoint:

  • /quotes/v1/swap: Returns the swap parameters for a route

This endpoint is a POST request.

3

Executing a Swap

After getting the swap parameters, you can execute the swap by calling the swap router contract with the necessary parameters.


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