What You’ll Achieve
- Earn 20-200% APY from trading fees
- Keep your tokens liquid (withdraw anytime)
- Concentrate liquidity where it’s most profitable
- Manage positions with minimal maintenance
When to Use Concentrated Liquidity
- You want higher yields than staking offers
- You can monitor and adjust positions occasionally
- You believe in the long-term value of both tokens
- You want to profit from high trading volume pairs
How Your Position Earns Money
Unlike traditional liquidity pools where your tokens are spread across all prices, DLMM lets you concentrate liquidity in specific price ranges where trading actually happens. Example: SOL trading at $100- Wide range (80−120): Lower fees but always active
- Tight range (98−102): Higher fees but needs monitoring
- Current price focus (99.50−100.50): Maximum fees, maximum attention
- Price is within your chosen range
- Traders swap through your liquidity bins
- You collect accumulated fees anytime
- Price moves outside your range
- You still own the tokens, just no fee income
- Rebalance to start earning again
Create Your First Position
Set up a position to earn fees on SOL-USDC trading:Copy
import { LiquidityBookServices, MODE } from '@saros-finance/dlmm-sdk';
import { Connection, PublicKey, Keypair } from '@solana/web3.js';
import { BN } from '@coral-xyz/anchor';
class FeeEarningPosition {
private dlmmService: LiquidityBookServices;
private poolAddress: PublicKey;
constructor(poolAddress: PublicKey, rpcUrl?: string) {
this.poolAddress = poolAddress;
this.dlmmService = new LiquidityBookServices({
mode: MODE.MAINNET, // or MODE.DEVNET
options: {
rpcUrl: rpcUrl || "https://api.mainnet-beta.solana.com"
}
});
}
/**
* Step 1: Create position to earn fees
*/
async createPosition({
user,
lowerBinId,
upperBinId,
tokenXAmount,
tokenYAmount,
maxSlippage = 0.005 // 0.5%
}: {
user: Keypair;
lowerBinId: number;
upperBinId: number;
tokenXAmount: number;
tokenYAmount: number;
maxSlippage?: number;
}) {
// Calculate price range around current market price
try {
const poolState = await this.dlmmPool.getPoolState();
// Distribute liquidity optimally across your price range
const binLiquidityDistribution = await this.calculateLiquidityDistribution(
lowerBinId,
upperBinId,
tokenXAmount,
tokenYAmount,
poolState.activeId
);
// Create your fee-earning position
const createPositionTx = await this.dlmmPool.initializePositionAndAddLiquidity({
user: user.publicKey,
lowerBinId,
upperBinId,
tokenXAmount: new BN(tokenXAmount * Math.pow(10, 6)),
tokenYAmount: new BN(tokenYAmount * Math.pow(10, 9)),
binLiquidityDist: binLiquidityDistribution
});
const signature = await this.connection.sendTransaction(createPositionTx, [user]);
await this.connection.confirmTransaction(signature);
const position = await this.getPositionByOwner(user.publicKey);
return { signature, position };
} catch (error) {
throw new Error(`Position creation failed: ${error}`);
}
}
/**
* Step 2: Add more liquidity to increase earnings
*/
async addLiquidityToPosition({
user,
positionAddress,
tokenXAmount,
tokenYAmount
}: {
user: Keypair;
positionAddress: PublicKey;
tokenXAmount: number;
tokenYAmount: number;
}) {
// Adding liquidity increases your fee earnings proportionally
const position = await this.dlmmPool.getPosition(positionAddress);
const binLiquidityDistribution = await this.calculateLiquidityDistribution(
position.lowerBinId,
position.upperBinId,
tokenXAmount,
tokenYAmount,
(await this.dlmmPool.getPoolState()).activeId
);
const addLiquidityTx = await this.dlmmPool.addLiquidityByStrategy({
user: user.publicKey,
position: positionAddress,
tokenXAmount: new BN(tokenXAmount * Math.pow(10, 6)),
tokenYAmount: new BN(tokenYAmount * Math.pow(10, 9)),
binLiquidityDist: binLiquidityDistribution
});
const signature = await this.connection.sendTransaction(addLiquidityTx, [user]);
await this.connection.confirmTransaction(signature);
return { signature };
}
/**
* Step 3: Collect your earned fees
*/
async withdrawLiquidity({
user,
positionAddress,
binIds,
liquidityAmounts
}: {
user: Keypair;
positionAddress: PublicKey;
binIds: number[];
liquidityAmounts: BN[];
}) {
const removeLiquidityTx = await this.dlmmPool.removeLiquidity({
user: user.publicKey,
position: positionAddress,
binIds,
liquidityAmounts
});
const signature = await this.connection.sendTransaction(removeLiquidityTx, [user]);
await this.connection.confirmTransaction(signature);
return { signature };
}
/**
* Step 4: Collect your earned trading fees
*/
async collectFees({
user,
positionAddress
}: {
user: Keypair;
positionAddress: PublicKey;
}) {
const position = await this.dlmmPool.getPosition(positionAddress);
const feeInfo = await this.calculatePositionFees(position);
// Claim your earned fees
const collectFeesTx = await this.dlmmPool.claimFee({
user: user.publicKey,
position: positionAddress
});
const signature = await this.connection.sendTransaction(collectFeesTx, [user]);
await this.connection.confirmTransaction(signature);
return {
signature,
feesEarned: {
tokenX: feeInfo.feeX.toNumber() / Math.pow(10, 6),
tokenY: feeInfo.feeY.toNumber() / Math.pow(10, 9)
}
};
}
/**
* Close position and withdraw everything
*/
async closePosition({
user,
positionAddress
}: {
user: Keypair;
positionAddress: PublicKey;
}) {
// Collect any remaining fees first
await this.collectFees({ user, positionAddress });
// Get all liquidity to withdraw
const position = await this.dlmmPool.getPosition(positionAddress);
const binIds: number[] = [];
const liquidityAmounts: BN[] = [];
for (const bin of position.userPositions) {
if (bin.liquidityShare.gt(new BN(0))) {
binIds.push(bin.binId);
liquidityAmounts.push(bin.liquidityShare);
}
}
// Withdraw all liquidity
if (binIds.length > 0) {
await this.withdrawLiquidity({
user,
positionAddress,
binIds,
liquidityAmounts
});
}
// Close the position
const closePositionTx = await this.dlmmPool.closePosition({
user: user.publicKey,
position: positionAddress
});
const signature = await this.connection.sendTransaction(closePositionTx, [user]);
await this.connection.confirmTransaction(signature);
return { signature };
}
/**
* Check position performance
*/
async checkPositionStatus(positionAddress: PublicKey) {
const position = await this.dlmmPool.getPosition(positionAddress);
const poolState = await this.dlmmPool.getPoolState();
const feeInfo = await this.calculatePositionFees(position);
const currentValue = await this.calculatePositionValue(position);
const isInRange = poolState.activeId >= position.lowerBinId &&
poolState.activeId <= position.upperBinId;
const daysActive = (Date.now() - position.createdAt * 1000) / (1000 * 60 * 60 * 24);
const dailyFeeReturn = (feeInfo.feeX.toNumber() + feeInfo.feeY.toNumber()) / currentValue.totalValue;
const estimatedAPY = (dailyFeeReturn * 365) * 100;
return {
isEarningFees: isInRange,
feesEarned: {
tokenX: feeInfo.feeX.toNumber() / Math.pow(10, 6),
tokenY: feeInfo.feeY.toNumber() / Math.pow(10, 9)
},
estimatedAPY: estimatedAPY,
needsRebalancing: !isInRange
};
}
/**
* Rebalance position to new price range
*/
async rebalancePosition({
user,
oldPositionAddress,
newLowerBinId,
newUpperBinId
}: {
user: Keypair;
oldPositionAddress: PublicKey;
newLowerBinId: number;
newUpperBinId: number;
}) {
console.log('Rebalancing position to new range:', {
old: oldPositionAddress.toString(),
newRange: `${newLowerBinId} to ${newUpperBinId}`
});
try {
// Close old position and get tokens back
const { signature: closeSignature } = await this.closePosition({
user,
positionAddress: oldPositionAddress
});
// Wait a bit for transaction to settle
await new Promise(resolve => setTimeout(resolve, 2000));
// Create new position with the same tokens
const oldPosition = await this.dlmmPool.getPosition(oldPositionAddress);
const tokenBalances = await this.calculatePositionValue(oldPosition);
const { signature: createSignature, position: newPosition } = await this.createPosition({
user,
lowerBinId: newLowerBinId,
upperBinId: newUpperBinId,
tokenXAmount: tokenBalances.tokenX / Math.pow(10, 6),
tokenYAmount: tokenBalances.tokenY / Math.pow(10, 9)
});
console.log('Position rebalanced successfully:', {
closeSignature,
createSignature,
newPosition: newPosition?.address
});
return {
closeSignature,
createSignature,
newPosition
};
} catch (error) {
console.error('Failed to rebalance position:', error);
throw error;
}
}
// Helper methods
private async calculateLiquidityDistribution(
lowerBinId: number,
upperBinId: number,
tokenXAmount: number,
tokenYAmount: number,
activeBinId: number
) {
// Concentrated liquidity distribution - more liquidity near active bin
const binLiquidityDist = [];
const totalBins = upperBinId - lowerBinId + 1;
for (let binId = lowerBinId; binId <= upperBinId; binId++) {
const distanceFromActive = Math.abs(binId - activeBinId);
const weight = Math.exp(-distanceFromActive * 0.1); // Exponential decay
binLiquidityDist.push({
binId,
xAmountBpsOfTotal: Math.floor((weight / totalBins) * tokenXAmount * 10000),
yAmountBpsOfTotal: Math.floor((weight / totalBins) * tokenYAmount * 10000)
});
}
return binLiquidityDist;
}
private async calculatePositionFees(position: any) {
// Calculate accumulated fees from position
let totalFeeX = new BN(0);
let totalFeeY = new BN(0);
for (const binPosition of position.userPositions) {
if (binPosition.liquidityShare.gt(new BN(0))) {
// Get bin data to calculate fee share
const bin = await this.dlmmPool.getBin(binPosition.binId);
const feeShare = binPosition.liquidityShare.div(bin.liquiditySupply);
totalFeeX = totalFeeX.add(bin.feeAmountXPerTokenComplete.mul(feeShare));
totalFeeY = totalFeeY.add(bin.feeAmountYPerTokenComplete.mul(feeShare));
}
}
return { feeX: totalFeeX, feeY: totalFeeY };
}
private async calculatePositionValue(position: any) {
let totalTokenX = 0;
let totalTokenY = 0;
for (const binPosition of position.userPositions) {
if (binPosition.liquidityShare.gt(new BN(0))) {
const bin = await this.dlmmPool.getBin(binPosition.binId);
const share = binPosition.liquidityShare.toNumber() / bin.liquiditySupply.toNumber();
totalTokenX += bin.amountX.toNumber() * share;
totalTokenY += bin.amountY.toNumber() * share;
}
}
// Calculate USD value (mock prices for example)
const tokenXPrice = 1; // USDC price
const tokenYPrice = 105; // SOL price
const totalValue = (totalTokenX / Math.pow(10, 6)) * tokenXPrice +
(totalTokenY / Math.pow(10, 9)) * tokenYPrice;
return { tokenX: totalTokenX, tokenY: totalTokenY, totalValue };
}
private binIdToPrice(binId: number): number {
// Convert bin ID to actual price (simplified)
const binStep = 25; // 0.25% bin step
return Math.pow(1 + binStep / 10000, binId - 8388608);
}
private async getPositionByOwner(owner: PublicKey) {
// Get user's positions
const positions = await this.dlmmPool.getPositionsByUser(owner);
return positions[positions.length - 1]; // Return latest position
}
}
## Start Earning Fees
```typescript
// Set up your position manager
const connection = new Connection('https://api.devnet.solana.com');
const poolAddress = new PublicKey('YourPoolAddress');
const user = Keypair.generate(); // Your wallet
const positionManager = new FeeEarningPosition(connection, poolAddress);
// 1. Create position around current price (±2%)
const { signature, position } = await positionManager.createPosition({
user,
lowerBinId: 8388500, // 2% below current
upperBinId: 8388700, // 2% above current
tokenXAmount: 1000, // 1000 USDC
tokenYAmount: 10, // 10 SOL
});
console.log(`✅ Position created: ${signature}`);
// 2. Check if you're earning fees
const status = await positionManager.checkPositionStatus(
new PublicKey(position.address)
);
console.log(`Earning fees: ${status.isEarningFees}`);
console.log(`Estimated APY: ${status.estimatedAPY.toFixed(1)}%`);
console.log(`Fees earned so far:`, status.feesEarned);
// 3. Collect fees anytime
if (status.feesEarned.tokenX > 1) { // If earned >1 USDC
const { signature: claimSig, feesEarned } = await positionManager.collectFees({
user,
positionAddress: new PublicKey(position.address)
});
console.log(`💰 Collected fees: ${feesEarned.tokenX} USDC, ${feesEarned.tokenY} SOL`);
}
Copy
## Optimize Your Earnings
### Choose Your Range Strategy
**Conservative (±5% range)**:
- Lower fees but position stays active longer
- Less monitoring required
- Good for stable pairs
**Aggressive (±1% range)**:
- Higher fees when active
- Needs frequent rebalancing
- Best for volatile pairs with tight spreads
**Balanced (±2-3% range)**:
- Good middle ground for most pairs
- Reasonable fees with manageable maintenance
### When to Rebalance
```typescript
// Check if rebalancing would be profitable
const shouldRebalance = async (positionAddress: PublicKey) => {
const status = await positionManager.checkPositionStatus(positionAddress);
// Rebalance if:
return (
!status.isEarningFees && // Out of range
status.feesEarned.tokenX > 5 // Earned enough to cover gas
);
};
Manage Your Risk
Understand Impermanent Loss
- Your tokens shift ratio as price moves
- IL matters less if you earn enough fees
- Tight ranges have higher IL but higher fees
- Track your net position: fees earned - IL
Position Monitoring
- Daily: Check if position is in range
- Weekly: Review fee accumulation vs gas costs
- Monthly: Assess overall performance vs alternatives
- As needed: Rebalance when profitable
Risk Management Tips
- Start with smaller amounts to learn
- Use wider ranges initially (±3-5%)
- Don’t put all funds in one position
- Set alerts for when positions go out of range