For the complete documentation index, see llms.txt. This page is also available as Markdown.
Managing positions: code examples
Code examples for the full concentrated liquidity position lifecycle: reading pool state, opening a position, monitoring it, claiming fees and rewards, and withdrawing.
This page walks through the full lifecycle of a concentrated liquidity position using the Stellar SDK. Make sure you're familiar with Prerequisites & basics — the RPC endpoints, contract addresses and data conversion helpers used below come from there.
All position management calls go directly to the pool contract. You can find the pool address on the pool page of the Aquarius website or programmatically — see Get pools info. Concentrated pools report pool_type: "concentrated" both on-chain (get_info) and in the backend API pool listing — filter the list by that field on your side.
Helper functions used in the snippets below (scval_map, simulate) are defined once in the complete code at the end of this page.
Step 1. Read the pool state
Everything starts with two read-only values: the current tick (from get_slot0) and the tick spacing. Read-only calls are executed through transaction simulation — no signature required.
# Current price state: {"sqrt_price_x96": U256, "tick": i32}slot0 =scval_map(simulate("get_slot0"))current_tick = slot0["tick"].i32.int32# Tick spacing: 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)tick_spacing =simulate("get_tick_spacing").i32.int32# Human-readable price of token0 in token1 unitscurrent_price =1.0001** current_tickprint(f"tick={current_tick} spacing={tick_spacing} price={current_price:.7f}")
// Current price state: { sqrt_price_x96: BigInt, tick: number }constslot0=awaitsimulate('get_slot0');constcurrentTick=slot0.tick;// Tick spacing: 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)consttickSpacing=awaitsimulate('get_tick_spacing');// Human-readable price of token0 in token1 unitsconstcurrentPrice=Math.pow(1.0001,currentTick);console.log(`tick=${currentTick} spacing=${tickSpacing} price=${currentPrice}`);
The price is expressed as token1 per token0 in raw (stroop) units, where token0/token1 are the pool tokens sorted by contract ID (get_tokens returns them in order). Since Stellar classic assets have 7 decimals on both sides, the raw ratio matches the display price.
Step 2. Convert your price range to ticks
Position boundaries are ticks: price(tick) = 1.0001^tick, and both boundaries must be multiples of the tick spacing. The helper below picks the nearest valid ticks for a target price range — here, ±5% around the current price.
Step 3. Estimate and open the position
estimate_deposit_position computes how much of each token the deposit will actually consume and how much liquidity you'll receive. Use the estimated liquidity to set a min_liquidity slippage guard, then call deposit_position.
The desired_amounts you pass are maximums: the contract pulls what the current price requires and refunds the rest in the same transaction. If the position range lies entirely above or below the current price, only one of the two tokens is needed — pass 0 for the other.
Depositing into the same (tick_lower, tick_upper) range again adds to the existing position. One account can hold up to 20 positions per pool. The first deposit into an empty pool sets the initial pool price from the ratio of the amounts and must include both tokens.
Step 4. List your positions and check if they are in range
There are no position IDs — a position is identified by your address plus the tick range. get_user_position_snapshot lists all your ranges in the pool; a position keeps earning swap fees and AQUA rewards only while tick_lower <= current_tick < tick_upper. Keep in mind that the reward weight is checkpoint-based — it updates only when you deposit, withdraw or claim, so perform an action after the price crosses your range boundary to re-sync it.
The per-position calls used below (get_position, get_position_fees, claim_position_fees) fail with PositionNotFound if you pass a range you have no position in (for example, one you have already fully withdrawn) — take the ranges from the snapshot rather than hardcoding them.
Step 5. Check and claim swap fees
Fees accrue per position in both pool tokens. get_position_fees (read-only) shows the pending amounts; claim_position_fees transfers them to you. get_all_position_fees / claim_all_position_fees do the same across all your positions at once.
Step 6. Claim AQUA rewards
If the pool has active AMM rewards, in-range positions accrue AQUA. Check with get_user_reward and collect with claim.
Step 7. Withdraw the position
Read the position's liquidity with get_position, estimate the payout, then call withdraw_position. The payout includes the position's entire accrued swap fee balance on top of the principal — even when you withdraw only part of the liquidity; withdrawing the full liquidity closes the position. Use min_amounts as slippage guards (they are checked against the total payout, principal + fees).
To rebalance an out-of-range position, withdraw it and open a new one around the current price (Steps 1–3).
Complete code examples
The scripts below implement the shared simulate/invoke helpers used in the steps above and run through the whole lifecycle. Provide the secret key of a funded account and the address of a concentrated pool.
Copy the full code PythonCopy the full code JavaScript
import math
def price_to_tick(price: float, tick_spacing: int) -> int:
"""Convert a price to the nearest valid (spacing-aligned) tick below it."""
tick = math.floor(math.log(price) / math.log(1.0001))
return tick - tick % tick_spacing # Python's % keeps this aligned-down for negatives too
# Range: 5% below to 5% above the current price
tick_lower = price_to_tick(current_price * 0.95, tick_spacing)
tick_upper = price_to_tick(current_price * 1.05, tick_spacing) + tick_spacing
function priceToTick(price, tickSpacing) {
/** Convert a price to the nearest valid (spacing-aligned) tick below it. */
const tick = Math.floor(Math.log(price) / Math.log(1.0001));
// Align down to the spacing grid (handles negative ticks correctly)
return tick - ((tick % tickSpacing) + tickSpacing) % tickSpacing;
}
// Range: 5% below to 5% above the current price
const tickLower = priceToTick(currentPrice * 0.95, tickSpacing);
const tickUpper = priceToTick(currentPrice * 1.05, tickSpacing) + tickSpacing;
# 10 XLM and 10 USDC (raw 7-decimals amounts), in sorted token order
desired_amounts = [100_0000000, 100_0000000]
est = simulate("estimate_deposit_position", [
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
])
est_amounts = [u128_to_int(v.u128) for v in est.vec.sc_vec[0].vec.sc_vec]
est_liquidity = u128_to_int(est.vec.sc_vec[1].u128)
print(f"will pull {est_amounts}, mint liquidity {est_liquidity}")
# Allow 1% slippage on minted liquidity
min_liquidity = est_liquidity * 99 // 100
result = invoke("deposit_position", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
scval.to_uint128(min_liquidity),
])
deposited = [u128_to_int(v.u128) for v in result.vec.sc_vec[0].vec.sc_vec]
liquidity = u128_to_int(result.vec.sc_vec[1].u128)
print(f"deposited {deposited}, received liquidity {liquidity}")
fees = simulate("get_position_fees", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
])
pending = [u128_to_int(v.u128) for v in fees.vec.sc_vec]
print(f"pending fees: token0={pending[0]}, token1={pending[1]}")
if any(pending):
claimed = invoke("claim_position_fees", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
])
print("claimed:", [u128_to_int(v.u128) for v in claimed.vec.sc_vec])
position = scval_map(simulate("get_position", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
]))
position_liquidity = u128_to_int(position["liquidity"].u128)
est = simulate("estimate_withdraw_position", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
scval.to_uint128(position_liquidity),
])
est_amounts = [u128_to_int(v.u128) for v in est.vec.sc_vec]
# Allow 1% slippage
min_amounts = [a * 99 // 100 for a in est_amounts]
withdrawn = invoke("withdraw_position", [
scval.to_address(keypair.public_key),
scval.to_int32(tick_lower),
scval.to_int32(tick_upper),
scval.to_uint128(position_liquidity),
scval.to_vec([scval.to_uint128(a) for a in min_amounts]),
])
print("withdrawn:", [u128_to_int(v.u128) for v in withdrawn.vec.sc_vec])