Skip to main content
This page helps you move a position to a new price range, and, with the SDKs, track the trades that pass through a position’s range.

How a rebalance works

A rebalance replaces your position with a new one in the same pool in one transaction. It reuses the old position’s funds, including earned fees, and you can add funds from your wallet. The new position keeps the owner and withdrawal address and has a new ID. A rebalance does not swap one asset for the other. If the new range needs a different mix, you can add the asset it needs; anything the new position does not use returns to the withdrawal address. Check those amounts in the review before confirming. The reviewed amounts must still match when the transaction executes; there is no slippage setting. If pool activity makes the preview out of date, the rebalance can be rejected. Your old position stays unchanged, though a rejected transaction can still cost a network fee. Refresh the review before trying again.

Plan and submit a rebalance

Prerequisites: Your authenticated mainnet client for TypeScript or dex for Python from Setup, and an existing position with liquidity. The CLI has no rebalance command.Build the plan immediately before submission and submit it unchanged. The contract checks the amounts at execution, so pool activity can invalidate a plan. These examples recenter the range on the current tick, one spacing below and two above, funded only from what the old position returns.Replace POSITION_TOKEN_ID with the ID of the position you intend to change. Define the confirmation helper for your SDK before running these examples.

Step 1: Read the position and the pool

Step 2: Plan

maxFunding0: 0n, maxFunding1: 0n means reuse only what the old position returns. The planner recovers the principal at the current price plus all fees, including those not yet checkpointed, and sizes the new position from that alone. Nothing is spent.
Pass positive maxFunding0 and maxFunding1 budgets to add funds from your records, or liquidityTarget for an exact size.

Step 3: Submit the plan

Spreading the plan keeps the submitted numbers exactly the ones printed. The example allows 120 blocks for proving and inclusion; adjust that window for your prover. The SDK’s default 20-block deadline can expire before delegated proving finishes. A longer deadline does not relax the exact-amount checks. If execution rejects the transaction, the old position stays unchanged. Check the rejection details and build a fresh plan before retrying.

Track the fills a position takes

A position fill is the change in the two asset amounts backing a position’s liquidity between two consecutive pool prices. Each swap that moves the pool’s price through the position’s range changes that inventory, and the sequence of those changes is the position’s fill history. Fills describe inventory, not fees; fee earnings are separate.The TypeScript example lp-fill-tracker.ts replays pool trades against a position’s current range and liquidity, then follows live trades. It reads the position on chain, takes prices from REST trade history, uses Aleo blocks to order swaps and route legs, and listens for WebSocket messages to backfill new trades. It reads up to 20 swaps on start; the first establishes a starting price, leaving up to 19 inventory changes to report.Historical replay holds the current range and liquidity fixed. It can include swaps from before the position was minted and does not reconstruct earlier increases or decreases. Treat it as an inventory calculation for that configuration, not a record of fills the position actually received. Live tracking stops if the position’s range or liquidity changes.Run it from a Veil checkout with Node.js 22 or later and pnpm:
Replace YOUR_PRIVATE_KEY with the account’s private key and POSITION_TOKEN_ID with the position’s token ID from getOwnedPositions. Mainnet is the default; pass --network testnet to track a testnet position. Each fill it prints carries the trade and transaction IDs, the block and leg order, the position’s liquidity and range, the pool’s tick and square-root price before and after, the swap direction, and the position’s token0 and token1 amounts before and after.The example exports calculatePositionFill, which values a position’s liquidity and range at two prices without reading the network, and trackLiquidityPosition, which runs the full tracker. Import either into your own application. See the fill tracker source for the options it accepts.The Python SDK has no fill tracker.

How it works on chain

A rebalance runs through shield_swap_rebalance_router.aleo and replaces your position in one transaction. It reuses the old position’s principal and fees, adds any planned funding, and pays out the surplus. No swap takes place.

What changes

How the position is replaced

  1. The contract removes all old liquidity and settles pending fees. Recovered funds include the principal, newly settled fees, and any balance already owed.
  2. It checks the recovered amounts against the plan and closes the old position.
  3. It mints the successor using the recovered funds plus any planned funding.
  4. It pays any surplus to the withdrawal address. For each token, recovered funds plus funding must equal the amount used plus the refund.
A different range can need more of one token and less of the other. The router handles that with funding and refunds; it does not trade one token for the other.

When the plan no longer matches

Execution checks the recovered amounts and the amounts required for the planned successor liquidity. There is no slippage tolerance: if either token’s recovered or required amount differs from the plan, the transaction is rejected. It must also execute by the plan’s block-height deadline.If rejected, the old position stays unchanged. Check the rejection details and build a fresh plan before retrying. After acceptance, save the successor’s ID and record for later operations.Position accounting, both ranges, and the amounts are public; ownership and the withdrawal address are confidential. See Public data.

Next steps