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:| Field | Description |
|---|---|
poolId | Pool identifier (LP identity public key) |
owner | Your public key |
tickLower | Lower tick of your range |
tickUpper | Upper tick of your range |
liquidity | V3 liquidity units in your position |
uncollectedFeesA | Earned fees for asset A not yet claimed |
uncollectedFeesB | Earned fees for asset B not yet claimed |
assetAAddress | Token A identifier |
assetBAddress | Token B identifier |
inRange | Whether current price is within your range |
createdAt | Position creation timestamp |
Understanding Liquidity Units
Theliquidity 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:Rebalancing only supports moving to a tighter range (subset of the original). The new range must require less or equal capital. Rebalancing to a wider range that requires additional deposits is not supported.
async function rebalanceToTighterRange(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 a TIGHTER range inside the current position
// New range must be a subset of old range to avoid requiring additional deposits
const shrinkAmount = tickSpacing * 2;
const newLower = currentPosition.tickLower + shrinkAmount;
const newUpper = currentPosition.tickUpper - shrinkAmount;
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
UseamountAMin 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;
}
Next Steps
- Free Balances - Reduce overhead for frequent rebalancing
- Pool Creation - Launch new V3 pools
- Position Locks - Commit a position to the pool until expiry
- Error Handling - Handle failures gracefully