The add and remove liquidity functions enable users to earn LP fees by providing liquidity to pools.

Pool Types

Users can only add liquidity to constant product pools. Single-sided pools only receive an initial deposit of asset A, at which point they trade against a bonding curve until they bond. Constant product pools require an initial deposit of both assets.

Simulate Before Adding

Adding liquidity has to be done in the right ratio to maintain the pool’s price. Always simulate to check expected LP tokens and potential refunds:
async function simulateAddLiquidity() {
  try {
    const simulation = await client.simulateAddLiquidity({
      poolId: "pool-public-key",
      assetAAmount: "100000000",    // 1 BTC
      assetBAmount: "100000000000", // 100,000 USDB
    });

    console.log('LP tokens to mint:', simulation.lpTokensToMint);
    console.log('Pool share %:', simulation.poolSharePercentage);
    console.log('Asset A refund:', simulation.assetARefundAmount);
    console.log('Asset B refund:', simulation.assetBRefundAmount);
    
    return simulation;
  } catch (error) {
    console.error('Simulation failed:', error);
  }
}
If you add liquidity in the wrong ratio, you will be atomically refunded the excess asset.

Execute Liquidity Addition

async function addLiquidity() {
  try {
    // First, check the pool to understand the current ratio
    const pool = await client.getPool("pool-id");
    console.log('Current pool ratio:', pool.assetAReserve, ':', pool.assetBReserve);

    // Add liquidity maintaining the pool ratio
    const response = await client.addLiquidity({
      poolId: "pool-id",
      assetAAmount: "100000000",          // 1 BTC
      assetBAmount: "100000000000",       // 100,000 USDB
      assetAMinAmountIn: "100000000",     // set minimums to protect from price moves
      assetBMinAmountIn: "100000000000",
    });

    console.log('Liquidity added successfully!');
    console.log('LP tokens received:', response.lpTokensMinted);
    
    // Check if any assets were refunded
    if (response.refund) {
      console.log('Asset A refunded:', response.refund.assetAAmount);
      console.log('Asset B refunded:', response.refund.assetBAmount);
    }
    
    return response;
  } catch (error) {
    console.error('Failed to add liquidity:', error);
  }
}

Optimal Amount Calculation

Avoid refunds by calculating optimal amounts:
async function addLiquidityOptimal(poolId: string, assetADesired: bigint) {
  // Get current pool state
  const pool = await client.getPool(poolId);
  
  // Calculate optimal asset B amount based on current ratio
  const optimalAssetB = (assetADesired * BigInt(pool.assetBReserve)) / BigInt(pool.assetAReserve);
  
  console.log('Optimal amounts:', {
    assetA: assetADesired,
    assetB: optimalAssetB
  });
  
  // Add liquidity with optimal amounts
  const response = await client.addLiquidity({
    poolId: poolId,
    assetAAmount: assetADesired.toString(),
    assetBAmount: optimalAssetB.toString(),
    assetAMinAmountIn: assetADesired.toString(),
    assetBMinAmountIn: optimalAssetB.toString(),
  });
  
  console.log('Added liquidity with no refunds');
  return response;
}

Tracking Your Position

Monitor LP Holdings

async function trackPosition(poolId: string) {
  // Get your current position
  const position = await client.getLpPosition(poolId);
  
  console.log('LP tokens owned:', position.lpTokensOwned);
  console.log('Pool share:', position.sharePercentage, '%');
  console.log('Current value - Asset A:', position.valueAssetA);
  console.log('Current value - Asset B:', position.valueAssetB);
  
  // Check principal amounts if available
  if (position.principalAssetA !== undefined && position.principalAssetB !== undefined) {
    console.log('Initial deposit - Asset A:', position.principalAssetA);
    console.log('Initial deposit - Asset B:', position.principalAssetB);
  }
  
  // Check unrealized P&L if available
  if (position.unrealizedProfitLossAssetA && position.unrealizedProfitLossAssetB) {
    console.log('Unrealized P&L - Asset A:', position.unrealizedProfitLossAssetA);
    console.log('Unrealized P&L - Asset B:', position.unrealizedProfitLossAssetB);
  }
  
  return position;
}

Calculate Returns

Track your investment performance:
async function calculateReturns(poolId: string) {
  const position = await client.getLpPosition(poolId);
  
  // If principal amounts are available, calculate returns
  if (position.principalAssetA !== undefined && position.principalAssetB !== undefined) {
    const returnsA = position.valueAssetA - position.principalAssetA;
    const returnsB = position.valueAssetB - position.principalAssetB;
    
    // Calculate percentage returns
    const percentReturnA = position.principalAssetA > 0 
      ? (returnsA / position.principalAssetA) * 100 
      : 0;
    const percentReturnB = position.principalAssetB > 0 
      ? (returnsB / position.principalAssetB) * 100 
      : 0;
    
    console.log('Returns:');
    console.log(`Asset A: ${returnsA} (${percentReturnA.toFixed(2)}%)`);
    console.log(`Asset B: ${returnsB} (${percentReturnB.toFixed(2)}%)`);
  }
  
  // The API provides unrealized P&L directly
  if (position.unrealizedProfitLossAssetA && position.unrealizedProfitLossAssetB) {
    console.log('Unrealized P&L from API:');
    console.log(`Asset A: ${position.unrealizedProfitLossAssetA}`);
    console.log(`Asset B: ${position.unrealizedProfitLossAssetB}`);
  }
  
  // Check for impermanent loss
  const pool = await client.getPool(poolId);
  const currentPrice = Number(pool.reserveB) / Number(pool.reserveA);
  
  if (position.principalAssetA && position.principalAssetB) {
    const initialPrice = position.principalAssetB / position.principalAssetA;
    const priceChange = ((currentPrice - initialPrice) / initialPrice) * 100;
    console.log(`Price change since deposit: ${priceChange.toFixed(2)}%`);
  }
}

Removing Liquidity

Simulate Removal

Check what you’ll receive before removing:
async function simulateRemoveLiquidity(poolId: string, lpTokensToRemove: string) {
  try {
    const simulation = await client.simulateRemoveLiquidity({
      poolId: poolId,
      providerPublicKey: (await client.wallet.getIdentityPublicKey()),
      lpTokensToRemove: lpTokensToRemove,
    });
    
    console.log('Expected asset A:', simulation.assetAAmount);
    console.log('Expected asset B:', simulation.assetBAmount);
    
    return simulation;
  } catch (error) {
    console.error('Simulation failed:', error);
  }
}

Execute Removal

Remove liquidity and claim accumulated fees:
async function removeLiquidity(poolId: string, lpTokensToRemove: string) {
  try {
    // Check your LP balance first
    const position = await client.getLpPosition(poolId);
    const lpTokensOwned = BigInt(position.lpTokensOwned);
    const tokensToRemove = BigInt(lpTokensToRemove);
    
    if (lpTokensOwned < tokensToRemove) {
      throw new Error(`Insufficient LP tokens. Owned: ${lpTokensOwned}, Requested: ${tokensToRemove}`);
    }
    
    // Remove liquidity
    const response = await client.removeLiquidity({
      poolId: poolId,
      lpTokensToRemove: lpTokensToRemove,
    });
    
    console.log('Liquidity removed successfully!');
    console.log('Asset A received:', response.assetAWithdrawn);
    console.log('Asset B received:', response.assetBWithdrawn);
    console.log('Transfers:', response.assetATransferId, response.assetBTransferId);
    
    return response;
  } catch (error) {
    console.error('Failed to remove liquidity:', error);
  }
}

Partial vs Full Removal

// Remove all liquidity
async function removeAllLiquidity(poolId: string) {
  const position = await client.getLpPosition(poolId);
  return await removeLiquidity(poolId, position.lpTokensOwned);
}

// Remove partial liquidity (e.g., 50%)
async function removePartialLiquidity(poolId: string, percentage: number) {
  const position = await client.getLpPosition(poolId);
  const lpTokensToRemove = (BigInt(position.lpTokensOwned) * BigInt(percentage)) / 100n;
  return await removeLiquidity(poolId, lpTokensToRemove.toString());
}

Complete Liquidity Lifecycle

async function completeLiquidityLifecycle() {
  const poolId = "pool-id";
  
  // 1. Add liquidity
  console.log('=== Adding Liquidity ===');
  const addResponse = await client.addLiquidity({
    poolId: poolId,
    assetAAmount: 100000000n,    // 1 BTC
    assetBAmount: 100000000000n, // 100,000 USDB
  });
  
  const initialInvestment = {
    assetA: 100000000n,
    assetB: 100000000000n,
  };
  
  // 2. Monitor position over time
  console.log('\n=== Monitoring Position ===');
  await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
  await trackPosition(poolId);
  
  // 3. Calculate returns
  console.log('\n=== Calculating Returns ===');
  await calculateReturns(poolId);
  
  // 4. Remove 50% of liquidity
  console.log('\n=== Removing 50% Liquidity ===');
  await removePartialLiquidity(poolId, 50);
  
  // 5. Check remaining position
  console.log('\n=== Remaining Position ===');
  await trackPosition(poolId);
}

Error Handling

async function safeAddLiquidity(poolId: string, assetAAmount: bigint, assetBAmount: bigint) {
  try {
    // Check balances first
    const balance = await client.getBalance();
    
    // Validate sufficient balance
    const pool = await client.getPool(poolId);
    const BTC_PUBKEY = "020202020202020202020202020202020202020202020202020202020202020202";
    const btcRequired = pool.assetAAddress === BTC_PUBKEY ? assetAAmount : 
                       pool.assetBAddress === BTC_PUBKEY ? assetBAmount : 0n;
    
    if (balance.balance < btcRequired) {
      throw new Error('Insufficient BTC balance');
    }
    
    // Add liquidity
    return await client.addLiquidity({
      poolId: poolId,
      assetAAmount: assetAAmount,
      assetBAmount: assetBAmount,
    });
    
  } catch (error) {
    if (error.message.includes('Insufficient balance')) {
      console.error('Not enough funds');
    } else if (error.message.includes('Pool inactive')) {
      console.error('Pool is currently inactive');
    } else if (error.message.includes('Below minimum')) {
      console.error('Amount below minimum requirement');
    } else {
      console.error('Operation failed:', error);
    }
    throw error;
  }
}

Best Practices

  1. Always simulate first: Check expected outcomes before executing
  2. Track impermanent loss: Understand the risks of providing liquidity
  3. Regular monitoring: Check your position and fees periodically

Next Steps