Skip to main content
V3 positions are non-fungible. Each position is uniquely identified by the pool, your public key, and the tick range. You can have multiple positions in the same pool at different ranges.

List Your Positions

async function listPositions() {
  const response = await client.listConcentratedPositions();

  console.log(`Total positions: ${response.totalCount}`);

  for (const position of response.positions) {
    console.log(`Pool: ${position.poolId}`);
    console.log(`  Range: ${position.tickLower} to ${position.tickUpper}`);
    console.log(`  Liquidity: ${position.liquidity}`);
    console.log(`  In range: ${position.inRange}`);
    console.log(`  Uncollected fees: ${position.uncollectedFeesA} A, ${position.uncollectedFeesB} B`);
  }

  return response.positions;
}

Filter by Pool

const response = await client.listConcentratedPositions({
  poolId: "specific-pool-id",
});

Pagination

const response = await client.listConcentratedPositions({
  page: 2,
  pageSize: 50,  // max 100
});

console.log(`Page ${response.page} of ${Math.ceil(response.totalCount / response.pageSize)}`);

Position Data

Each position includes:
FieldDescription
poolIdPool identifier (LP identity public key)
ownerYour public key
tickLowerLower tick of your range
tickUpperUpper tick of your range
liquidityV3 liquidity units in your position
uncollectedFeesAEarned fees for asset A not yet claimed
uncollectedFeesBEarned fees for asset B not yet claimed
assetAAddressToken A identifier
assetBAddressToken B identifier
inRangeWhether current price is within your range
createdAtPosition creation timestamp

Understanding Liquidity Units

The liquidity field is a V3 math value, not a token amount. To understand what assets you have:
import { V3TickMath } from '@flashnet/sdk';

function estimatePositionValue(
  liquidity: string,
  tickLower: number,
  tickUpper: number,
  currentTick: number
) {
  // If out of range below, position is 100% asset A
  if (currentTick < tickLower) {
    console.log('Position is 100% asset A (below range)');
  }
  // If out of range above, position is 100% asset B
  else if (currentTick >= tickUpper) {
    console.log('Position is 100% asset B (above range)');
  }
  // If in range, mixed
  else {
    console.log('Position is mixed (in range)');
  }

  // For precise amounts, use getPoolLiquidity which returns range data
}

Monitoring Position Health

Check Range Status

async function checkPositionHealth(poolId: string, tickLower: number, tickUpper: number) {
  const positions = await client.listConcentratedPositions({ poolId });

  const position = positions.positions.find(
    p => p.tickLower === tickLower && p.tickUpper === tickUpper
  );

  if (!position) {
    throw new Error('Position not found');
  }

  if (position.inRange) {
    console.log('Position is earning fees');
  } else {
    console.log('Position is out of range - consider rebalancing');
  }

  // Check uncollected fees
  const hasSignificantFees =
    BigInt(position.uncollectedFeesA) > 0n ||
    BigInt(position.uncollectedFeesB) > 0n;

  if (hasSignificantFees) {
    console.log('Consider collecting fees:', {
      feeA: position.uncollectedFeesA,
      feeB: position.uncollectedFeesB,
    });
  }

  return position;
}

Calculate Range as Prices

import { V3TickMath } from '@flashnet/sdk';

function getPositionPriceRange(
  tickLower: number,
  tickUpper: number,
  baseDecimals: number,
  quoteDecimals: number
) {
  const priceLower = V3TickMath.tickToHumanPrice({
    tick: tickLower,
    baseDecimals,
    quoteDecimals,
  });

  const priceUpper = V3TickMath.tickToHumanPrice({
    tick: tickUpper,
    baseDecimals,
    quoteDecimals,
  });

  console.log(`Price range: $${priceLower} - $${priceUpper}`);
  return { priceLower, priceUpper };
}

Position Lifecycle

Create New Position

async function createPosition(
  poolId: string,
  priceLower: string,
  priceUpper: string,
  amountA: string,
  amountB: string
) {
  // Convert prices to ticks
  const range = V3TickMath.tickRangeFromPrices({
    priceLower,
    priceUpper,
    baseDecimals: 8,
    quoteDecimals: 6,
    tickSpacing: 60,
  });

  const response = await client.increaseLiquidity({
    poolId,
    tickLower: range.tickLower,
    tickUpper: range.tickUpper,
    amountADesired: amountA,
    amountBDesired: amountB,
    amountAMin: "0",
    amountBMin: "0",
  });

  console.log('Position created');
  console.log('Liquidity:', response.liquidityAdded);
  console.log('Used:', response.amountAUsed, 'A,', response.amountBUsed, 'B');

  return response;
}

Add to Existing Position

Adding liquidity to the same tick range increases your existing position:
async function addToPosition(
  poolId: string,
  tickLower: number,
  tickUpper: number,
  additionalA: string,
  additionalB: string
) {
  const response = await client.increaseLiquidity({
    poolId,
    tickLower,
    tickUpper,
    amountADesired: additionalA,
    amountBDesired: additionalB,
    amountAMin: "0",
    amountBMin: "0",
  });

  console.log('Added to position');
  console.log('New total liquidity increase:', response.liquidityAdded);

  return response;
}

Collect Fees

async function collectPositionFees(poolId: string, tickLower: number, tickUpper: number) {
  const response = await client.collectFees({
    poolId,
    tickLower,
    tickUpper,
  });

  console.log('Fees collected');
  console.log('Asset A:', response.feesCollectedA);
  console.log('Asset B:', response.feesCollectedB);

  return response;
}

Remove Partial Liquidity

async function removePartial(
  poolId: string,
  tickLower: number,
  tickUpper: number,
  liquidityToRemove: string
) {
  const response = await client.decreaseLiquidity({
    poolId,
    tickLower,
    tickUpper,
    liquidityToRemove,
    amountAMin: "0",
    amountBMin: "0",
  });

  console.log('Removed:', response.liquidityRemoved, 'liquidity');
  console.log('Received:', response.amountA, 'A,', response.amountB, 'B');
  console.log('Fees:', response.feesCollectedA, 'A,', response.feesCollectedB, 'B');

  return response;
}

Close Position

Pass "0" as liquidity to remove everything:
async function closePosition(poolId: string, tickLower: number, tickUpper: number) {
  const response = await client.decreaseLiquidity({
    poolId,
    tickLower,
    tickUpper,
    liquidityToRemove: "0",  // Remove all
    amountAMin: "0",
    amountBMin: "0",
  });

  console.log('Position closed');
  console.log('Total received:', response.amountA, 'A,', response.amountB, 'B');
  console.log('Final fees:', response.feesCollectedA, 'A,', response.feesCollectedB, 'B');

  return response;
}

Rebalance to New Range

When price moves out of your range, rebalance atomically:
async function rebalanceToCurrentPrice(poolId: string, currentPosition: {
  tickLower: number;
  tickUpper: number;
}) {
  // Get current pool state
  const liquidity = await client.getPoolLiquidity(poolId);
  const currentTick = liquidity.currentTick;
  const tickSpacing = liquidity.tickSpacing;

  // Create new range centered on current price
  const rangeWidth = currentPosition.tickUpper - currentPosition.tickLower;
  const newLower = V3TickMath.roundTickDown(currentTick - rangeWidth / 2, tickSpacing);
  const newUpper = V3TickMath.roundTickUp(currentTick + rangeWidth / 2, tickSpacing);

  const response = await client.rebalancePosition({
    poolId,
    oldTickLower: currentPosition.tickLower,
    oldTickUpper: currentPosition.tickUpper,
    newTickLower: newLower,
    newTickUpper: newUpper,
    liquidityToMove: "0",  // Move all
  });

  console.log('Rebalanced');
  console.log('New range:', newLower, 'to', newUpper);
  console.log('Old liquidity:', response.oldLiquidity);
  console.log('New liquidity:', response.newLiquidity);

  return response;
}

Multiple Positions Strategy

You can deploy capital across multiple ranges:
async function deployMultipleRanges(poolId: string, totalAmountA: string, totalAmountB: string) {
  const ranges = [
    { priceLower: "80000", priceUpper: "85000", weight: 20 },
    { priceLower: "85000", priceUpper: "95000", weight: 60 },
    { priceLower: "95000", priceUpper: "100000", weight: 20 },
  ];

  const totalWeight = ranges.reduce((sum, r) => sum + r.weight, 0);

  for (const range of ranges) {
    const amountA = (BigInt(totalAmountA) * BigInt(range.weight) / BigInt(totalWeight)).toString();
    const amountB = (BigInt(totalAmountB) * BigInt(range.weight) / BigInt(totalWeight)).toString();

    const tickRange = V3TickMath.tickRangeFromPrices({
      priceLower: range.priceLower,
      priceUpper: range.priceUpper,
      baseDecimals: 8,
      quoteDecimals: 6,
      tickSpacing: 60,
    });

    await client.increaseLiquidity({
      poolId,
      tickLower: tickRange.tickLower,
      tickUpper: tickRange.tickUpper,
      amountADesired: amountA,
      amountBDesired: amountB,
      amountAMin: "0",
      amountBMin: "0",
    });

    console.log(`Created position at ${range.priceLower}-${range.priceUpper}`);
  }
}

Slippage Protection

Use amountAMin and amountBMin to protect against price movement:
async function addLiquidityWithSlippage(
  poolId: string,
  tickLower: number,
  tickUpper: number,
  amountA: string,
  amountB: string,
  slippagePercent: number = 1  // 1% default
) {
  const slippageMultiplier = (100 - slippagePercent) / 100;

  const response = await client.increaseLiquidity({
    poolId,
    tickLower,
    tickUpper,
    amountADesired: amountA,
    amountBDesired: amountB,
    amountAMin: Math.floor(Number(amountA) * slippageMultiplier).toString(),
    amountBMin: Math.floor(Number(amountB) * slippageMultiplier).toString(),
  });

  return response;
}
If actual amounts fall below minimums due to price movement, the transaction fails and funds are returned.

Next Steps