Skip to main content
Concentrated liquidity (V3) allows you to provide liquidity within a specific price range instead of across all prices. Capital concentrated in active trading ranges earns more fees per dollar deployed.

V3 and V2 Pools

V3 pools complement the existing V2 constant product and bonding curve pools. Both pool types coexist:
  • V2 Constant Product: Liquidity spread across all prices, fungible LP tokens, simpler management
  • V2 Bonding Curve: Single-sided pools that bond to constant product
  • V3 Concentrated: Custom price ranges, non-fungible positions, higher capital efficiency
All pool types appear in listPools(). V3 pools have curveType: "V3_CONCENTRATED" and include additional fields like currentTick, tickSpacing, and totalLiquidity.
const pools = await client.listPools();

for (const pool of pools.pools) {
  if (pool.curveType === 'V3_CONCENTRATED') {
    console.log('V3 pool:', pool.lpPublicKey);
    console.log('  Current tick:', pool.currentTick);
    console.log('  Tick spacing:', pool.tickSpacing);
    console.log('  Positions:', pool.positionCount);
  } else {
    console.log('V2 pool:', pool.lpPublicKey, pool.curveType);
  }
}
V3 pools also have dedicated endpoints for detailed data: getPoolLiquidity() for depth chart visualization and getPoolTicks() for swap simulation.

How V3 Works

In V2 constant product pools, liquidity spreads from zero to infinity. Most of it sits idle outside the trading range. V3 pools let you choose where your capital works. The math uses ticks to represent prices. Each tick is a 0.01% price change from the previous tick:
price = 1.0001^tick
Your position is defined by a lower tick and upper tick. When the current price is within your range, your liquidity earns fees. When price moves outside, your position becomes single-sided and stops earning.

Quick Comparison

AspectV2 PoolsV3 Pools
Liquidity Range0 to infinityCustom price range
Capital Efficiency~50% activeUp to 4000x better
Position TypeFungible LP tokensNon-fungible positions
Fee TrackingGlobal per poolPer-position

Getting Started

Create a Position

async function createPosition() {
  const response = await client.increaseLiquidity({
    poolId: "pool-public-key",
    tickLower: -23040,           // Lower price bound
    tickUpper: -22980,           // Upper price bound
    amountADesired: "100000000", // 1 BTC in satoshis
    amountBDesired: "100000000", // 100,000 USDB in base units
    amountAMin: "99000000",      // 1% slippage protection
    amountBMin: "99000000",
  });

  console.log('Liquidity added:', response.liquidityAdded);
  console.log('Asset A used:', response.amountAUsed);
  console.log('Asset B used:', response.amountBUsed);
}

Convert Prices to Ticks

The SDK provides utilities to convert between human-readable prices and ticks:
import { V3TickMath } from '@flashnet/sdk';

// Convert a human price like "$90,000 per BTC" to a tick
const tick = V3TickMath.humanPriceToTick({
  humanPrice: "90000",
  baseDecimals: 8,   // BTC (base asset)
  quoteDecimals: 6,  // USDB (quote asset)
  tickSpacing: 60,
});

// Get a range around a target price
const range = V3TickMath.tickRangeFromPrices({
  priceLower: "85000",
  priceUpper: "95000",
  baseDecimals: 8,
  quoteDecimals: 6,
  tickSpacing: 60,
});

console.log('Tick range:', range.tickLower, 'to', range.tickUpper);

Core Operations

Add Liquidity

Creates a new position or adds to an existing one at the same tick range:
const response = await client.increaseLiquidity({
  poolId: "pool-public-key",
  tickLower: -23040,
  tickUpper: -22980,
  amountADesired: "100000000",
  amountBDesired: "100000000",
  amountAMin: "99000000",
  amountBMin: "99000000",
});

Remove Liquidity

Withdraws liquidity and collects accumulated fees:
const response = await client.decreaseLiquidity({
  poolId: "pool-public-key",
  tickLower: -23040,
  tickUpper: -22980,
  liquidityToRemove: "123456789",  // V3 liquidity units, or "0" for all
  amountAMin: "0",
  amountBMin: "0",
});

console.log('Principal returned:', response.amountA, response.amountB);
console.log('Fees collected:', response.feesCollectedA, response.feesCollectedB);

Collect Fees

Claim earned fees without removing liquidity:
const response = await client.collectFees({
  poolId: "pool-public-key",
  tickLower: -23040,
  tickUpper: -22980,
});

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

Rebalance Position

Move your liquidity to a new price range in a single atomic operation:
const response = await client.rebalancePosition({
  poolId: "pool-public-key",
  oldTickLower: -23040,
  oldTickUpper: -22980,
  newTickLower: -23100,
  newTickUpper: -22920,
  liquidityToMove: "0",  // 0 = move all
});

console.log('Old liquidity:', response.oldLiquidity);
console.log('New liquidity:', response.newLiquidity);
console.log('Fees collected:', response.feesCollectedA, response.feesCollectedB);

Pool Data

Get Liquidity Distribution

For building depth charts and visualizations:
const liquidity = await client.getPoolLiquidity("pool-public-key");

console.log('Current price:', liquidity.currentPrice);
console.log('Active liquidity:', liquidity.activeLiquidity);

// Iterate through liquidity ranges
for (const range of liquidity.ranges) {
  console.log(`${range.priceLower} - ${range.priceUpper}: ${range.liquidity}`);
  console.log(`  Status: ${range.status}`);  // "in_range", "below_price", or "above_price"
}

Get Pool Ticks

For swap simulation and slippage calculation:
const ticks = await client.getPoolTicks("pool-public-key");

console.log('Current tick:', ticks.currentTick);
console.log('Current liquidity:', ticks.currentLiquidity);
console.log('LP fee:', ticks.lpFeeBps, 'bps');

// Use ticks for simulation
for (const tick of ticks.ticks) {
  console.log(`Tick ${tick.tick}: net=${tick.liquidityNet}`);
}

Tick Spacing

Pools have a tick spacing that determines valid tick boundaries. Common values:
Tick SpacingPrice StepUse Case
100.1%Stable pairs, tight ranges
600.6%Most trading pairs
2002%Volatile pairs
Ticks must be divisible by the pool’s tick spacing:
// Round a tick to the nearest valid boundary
const validTick = V3TickMath.roundTick(arbitraryTick, tickSpacing);

// Round down (conservative for lower bounds)
const lower = V3TickMath.roundTickDown(tick, tickSpacing);

// Round up (conservative for upper bounds)
const upper = V3TickMath.roundTickUp(tick, tickSpacing);

// Raw price/tick conversions (without decimal adjustment)
const rawTick = V3TickMath.priceToTick(poolPrice);
const rawPrice = V3TickMath.tickToPrice(tick);

// Tick bounds
console.log('Valid tick range:', V3TickMath.MIN_TICK, 'to', V3TickMath.MAX_TICK);

Position States

Your position’s behavior depends on where the current price sits relative to your range:
Price LocationAsset AAsset BEarning Fees
Below your range100%0%No
Within your rangeMixedMixedYes
Above your range0%100%No
When price moves outside your range, your position converts entirely to one asset and stops earning fees until price returns.

Error Handling

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

try {
  await client.increaseLiquidity({ ... });
} catch (error) {
  if (isFlashnetError(error)) {
    console.log('Error:', error.errorCode);
    console.log('Message:', error.userMessage);

    // Check for fund recovery
    if (error.wasClawbackAttempted()) {
      const summary = error.clawbackSummary!;
      console.log(`Recovered: ${summary.successCount}/${summary.totalTransfers}`);
    }
  }
  throw error;
}

Next Steps