Skip to main content
V3 pools support free balances: per-user balances held within the pool that can be used for subsequent operations without additional Spark transfers. This reduces latency and overhead for market makers and frequent traders.

How It Works

When you withdraw liquidity, collect fees, or rebalance positions, you can retain the funds in a pool-level free balance instead of receiving a Spark transfer. These retained funds can then fund future operations like adding liquidity, all without leaving the pool.

Benefits

  • Reduced latency: No waiting for Spark transfer confirmations when redeploying capital
  • Lower overhead: Eliminates transfer fees for funds that stay in the pool
  • Atomic availability: Retained funds are immediately usable for the next operation
  • Simplified workflows: Compound fees or rebalance without capital leaving the system

Retaining Funds

On Decrease Liquidity

const response = await client.decreaseLiquidity({
  poolId: "pool-public-key",
  tickLower: -23040,
  tickUpper: -22980,
  liquidityToRemove: "0",  // All
  amountAMin: "0",
  amountBMin: "0",
  retainInBalance: true,   // Keep funds in free balance
});

console.log('Retained in balance:', response.retainedInBalance);
console.log('Current balance:', response.currentBalance);
// { balanceA: "1000000", balanceB: "500000" }

On Collect Fees

const response = await client.collectFees({
  poolId: "pool-public-key",
  tickLower: -23040,
  tickUpper: -22980,
  retainInBalance: true,
});

console.log('Fees retained:', response.feesCollectedA, response.feesCollectedB);
console.log('Current balance:', response.currentBalance);

On Rebalance

const response = await client.rebalancePosition({
  poolId: "pool-public-key",
  oldTickLower: -23040,
  oldTickUpper: -22980,
  newTickLower: -23100,
  newTickUpper: -22920,
  liquidityToMove: "0",
  retainInBalance: true,  // Keep any net excess in balance
});

Using Free Balances

Add Liquidity from Balance

Use useFreeBalanceA and useFreeBalanceB to fund operations from your retained balance:
const response = await client.increaseLiquidity({
  poolId: "pool-public-key",
  tickLower: -23100,
  tickUpper: -22920,
  amountADesired: "100000000",
  amountBDesired: "100000000",
  amountAMin: "0",
  amountBMin: "0",
  useFreeBalanceA: true,  // Draw from free balance first
  useFreeBalanceB: true,
});

console.log('Liquidity added:', response.liquidityAdded);
console.log('Amount A used:', response.amountAUsed);
console.log('Amount B used:', response.amountBUsed);
If your free balance is insufficient, the SDK automatically supplements with a Spark transfer for the difference.

Retain Excess on Add

When adding liquidity, any unused tokens (refunds) can be retained instead of returned via Spark:
const response = await client.increaseLiquidity({
  poolId: "pool-public-key",
  tickLower: -23100,
  tickUpper: -22920,
  amountADesired: "100000000",
  amountBDesired: "100000000",
  amountAMin: "0",
  amountBMin: "0",
  retainExcessInBalance: true,  // Keep refunds in balance
});

console.log('Liquidity added:', response.liquidityAdded);
console.log('Amount A refund:', response.amountARefund);
console.log('Amount B refund:', response.amountBRefund);
console.log('Retained in balance:', response.retainedInBalance);

Checking Balances

Single Pool

const balance = await client.getConcentratedBalance("pool-public-key");

console.log('Pool:', balance.poolId);
console.log('Asset A:', balance.assetAAddress);
console.log('Asset B:', balance.assetBAddress);
console.log('Balance A:', balance.balanceA);
console.log('Balance B:', balance.balanceB);
console.log('Available A:', balance.availableA);  // Total minus locked
console.log('Available B:', balance.availableB);
console.log('Locked A:', balance.lockedA);        // In-flight operations
console.log('Locked B:', balance.lockedB);

All Pools

const response = await client.getConcentratedBalances();

for (const balance of response.balances) {
  console.log(`Pool ${balance.poolId}:`);
  console.log(`  A: ${balance.balanceA} (${balance.availableA} available)`);
  console.log(`  B: ${balance.balanceB} (${balance.availableB} available)`);
}

Withdrawing Balances

To move funds from your free balance to your Spark wallet:
const response = await client.withdrawConcentratedBalance({
  poolId: "pool-public-key",
  amountA: "500000",  // Specific amount
  amountB: "max",     // All available
});

console.log('Withdrawn A:', response.amountAWithdrawn);
console.log('Withdrawn B:', response.amountBWithdrawn);
console.log('Remaining A:', response.remainingBalanceA);
console.log('Remaining B:', response.remainingBalanceB);
console.log('Transfer IDs:', response.outboundTransferIds);

Amount Options

ValueBehavior
"500000"Withdraw specific amount
"max" or "0"Withdraw entire balance
Empty stringSkip this asset

Common Workflows

Fee Compounding

Reinvest earned fees without Spark transfers:
async function compoundFees(poolId: string, tickLower: number, tickUpper: number) {
  // 1. Collect fees and retain
  await client.collectFees({
    poolId,
    tickLower,
    tickUpper,
    retainInBalance: true,
  });

  // 2. Check accumulated balance
  const balance = await client.getConcentratedBalance(poolId);
  console.log('Compounding:', balance.balanceA, 'A,', balance.balanceB, 'B');

  // 3. Add liquidity from balance
  await client.increaseLiquidity({
    poolId,
    tickLower,
    tickUpper,
    amountADesired: balance.availableA,
    amountBDesired: balance.availableB,
    amountAMin: "0",
    amountBMin: "0",
    useFreeBalanceA: true,
    useFreeBalanceB: true,
  });

  console.log('Fees compounded into position');
}

Zero-Transfer Rebalancing

Rebalance positions without any Spark overhead:
async function zeroTransferRebalance(
  poolId: string,
  oldLower: number,
  oldUpper: number,
  newLower: number,
  newUpper: number
) {
  // 1. Remove and retain
  await client.decreaseLiquidity({
    poolId,
    tickLower: oldLower,
    tickUpper: oldUpper,
    liquidityToRemove: "0",
    amountAMin: "0",
    amountBMin: "0",
    retainInBalance: true,
  });

  // 2. Check balance
  const balance = await client.getConcentratedBalance(poolId);

  // 3. Create new position from balance
  await client.increaseLiquidity({
    poolId,
    tickLower: newLower,
    tickUpper: newUpper,
    amountADesired: balance.availableA,
    amountBDesired: balance.availableB,
    amountAMin: "0",
    amountBMin: "0",
    useFreeBalanceA: true,
    useFreeBalanceB: true,
    retainExcessInBalance: true,
  });

  console.log('Rebalanced with zero Spark transfers');
}
Or use the atomic rebalance operation:
await client.rebalancePosition({
  poolId,
  oldTickLower: oldLower,
  oldTickUpper: oldUpper,
  newTickLower: newLower,
  newTickUpper: newUpper,
  liquidityToMove: "0",
  retainInBalance: true,  // Keep any net difference in balance
});

Market Maker Loop

High-frequency position management:
async function mmRebalanceLoop(poolId: string) {
  let currentLower = -23040;
  let currentUpper = -22980;

  while (true) {
    // Check if still in range
    const positions = await client.listConcentratedPositions({ poolId });
    const position = positions.positions.find(
      p => p.tickLower === currentLower && p.tickUpper === currentUpper
    );

    if (!position?.inRange) {
      // Get current price
      const liquidity = await client.getPoolLiquidity(poolId);
      const spacing = liquidity.tickSpacing;
      const tick = liquidity.currentTick;

      // Calculate new range
      const rangeWidth = currentUpper - currentLower;
      const newLower = Math.floor((tick - rangeWidth / 2) / spacing) * spacing;
      const newUpper = Math.ceil((tick + rangeWidth / 2) / spacing) * spacing;

      // Atomic rebalance, retain excess
      await client.rebalancePosition({
        poolId,
        oldTickLower: currentLower,
        oldTickUpper: currentUpper,
        newTickLower: newLower,
        newTickUpper: newUpper,
        liquidityToMove: "0",
        retainInBalance: true,
      });

      currentLower = newLower;
      currentUpper = newUpper;

      console.log(`Rebalanced to ${newLower}-${newUpper}`);
    }

    await sleep(60000);  // Check every minute
  }
}

Response Fields

All V3 liquidity operations include balance information when using free balance features:
FieldTypeDescription
retainedInBalancebooleanWhether funds were retained in balance
currentBalanceobjectUpdated balance after operation (balanceA, balanceB)
amountARefundstringExcess asset A returned (increase liquidity)
amountBRefundstringExcess asset B returned (increase liquidity)

Balance States

StateDescription
balanceTotal funds in your free balance
availableAmount you can use (total minus locked)
lockedFunds in pending operations

Next Steps