Skip to main content
You want to earn yield from your tokens, but traditional staking offers low returns and high risk. DLMM concentrated liquidity lets you earn trading fees by providing liquidity exactly where trades happen.

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 (8080-120): Lower fees but always active
  • Tight range (9898-102): Higher fees but needs monitoring
  • Current price focus (99.5099.50-100.50): Maximum fees, maximum attention
Your position earns fees when:
  1. Price is within your chosen range
  2. Traders swap through your liquidity bins
  3. You collect accumulated fees anytime
Your position stops earning when:
  • 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:
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`);
}

## 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