> ## Documentation Index
> Fetch the complete documentation index at: https://shield.fi/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracking position fills

> Track LP inventory changes and calculate a fill with Typescript or Python.

A fill measures how a swap changes the token0 and token1 backing your position. A positive change means you acquired that token; a negative change means you released it. Fees, deposits, and withdrawals are separate from fills.

## Track live fills

Use the authenticated clients from [Setup](https://shield.fi/docs/trading/configure.md) with the latest SDKs. Set `POSITION_TOKEN_ID` to your position's NFT token ID. The examples read pool swaps and print how much token0 and token1 your position gained or released. They do not submit transactions.

> [tab] Typescript
>
> Add these blocks to the same script, in order, using `client` and `publicClient` from Setup.
>
> **Read the position**
>
> The tracker uses the position's range and liquidity to calculate fills. `checkPosition` stops it if those values change, so a deposit or withdrawal is not counted as a swap.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> import { amountsForLiquidity, getSqrtPriceAtTickX128 } from "@provablehq/shield-swap-sdk"
> import type { Transaction } from "@provablehq/veil-core"
>
> type Position = NonNullable<Awaited<ReturnType<typeof client.getPosition>>>
> type Trade = Awaited<ReturnType<typeof client.api.getPoolTrades>>["data"][number]
>
> async function checkPosition(positionTokenId: string, original: Position) {
>   const current = await client.getPosition({ positionTokenId })
>   if (!current || current.pool !== original.pool ||
>       current.tick_lower !== original.tick_lower || current.tick_upper !== original.tick_upper ||
>       current.liquidity !== original.liquidity) {
>     throw new Error("Position changed; restart the tracker")
>   }
> }
> ```
>
> **Calculate the tokens backing the position**
>
> `inventoryAt` returns the position's token0 and token1 amounts at a given square-root price. We will call it before and after each swap to find the difference.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> function inventoryAt(position: Position, price: string) {
>   return amountsForLiquidity({
>     liquidity: position.liquidity,
>     sqrtLowerX128: getSqrtPriceAtTickX128(position.tick_lower),
>     sqrtUpperX128: getSqrtPriceAtTickX128(position.tick_upper),
>     sqrtPriceX128: BigInt(price),
>     roundUp: false,
>   })
> }
> ```
>
> **Fetch trades not yet seen**
>
> The API returns newest trades first. `readNewTrades` fetches pages until it reaches a known trade, then finishes reading every trade with that timestamp so none are skipped across pages. On startup, it reads the latest swap's full timestamp group to find which swap actually came last.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> function isSwap(trade: Trade) {
>   return trade.tradeType === "swap" || trade.tradeType === "swap_multi_hop"
> }
>
> async function readNewTrades(pool: string, seen: Set<string>, starting: boolean) {
>   const fresh = new Map<string, Trade>()
>   let overlapAt: Trade["executedAt"] | undefined
>   for (let offset = 0; ; offset += 100) {
>     const { data } = await client.api.getPoolTrades(pool, { limit: 100, offset })
>     for (const trade of data) if (!seen.has(trade.id)) fresh.set(trade.id, trade)
>     overlapAt ??= data.find(trade => seen.has(trade.id))?.executedAt
>     if (data.length < 100) {
>       if (seen.size && overlapAt === undefined) throw new Error("History gap; restart the tracker")
>       break
>     }
>     if (overlapAt !== undefined && data.at(-1)?.executedAt !== overlapAt) break
>     const latestSwap = [...fresh.values()].find(isSwap)
>     if (starting && latestSwap && data.at(-1)?.executedAt !== latestSwap.executedAt) break
>   }
>   return fresh
> }
> ```
>
> **Put swaps in execution order**
>
> Timestamps can tie. `swapOrder` checks that the transaction was accepted and locates the swap by block, transaction, program call, and hop. `orderedSwaps` drops non-swap activity and sorts the remaining trades oldest first; it stops if a price or an unambiguous program call is missing.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> type Order = [number, number, number, number]
> type OrderedSwap = { trade: Trade; order: Order }
>
> function compareOrder(a: Order, b: Order) {
>   return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]
> }
>
> async function swapOrder(trade: Trade): Promise<Order> {
>   const hash = await publicClient.findBlockHash({ transactionId: trade.transactionHash })
>   const block = await publicClient.getBlock({ hash })
>   const confirmed = block.transactions?.find(t => t.transaction.id === trade.transactionHash)
>   if (confirmed?.status !== "accepted") throw new Error("Swap is not accepted")
>   const transitions = (confirmed.transaction as Transaction).execution?.transitions ?? []
>   const matches = transitions.flatMap((t, i) =>
>     t.program === "shield_swap.aleo" && t.function === trade.tradeType ? [i] : [])
>   if (matches.length !== 1) throw new Error("Cannot resolve swap transition uniquely")
>   return [block.header.metadata.height, confirmed.index, matches[0], trade.legIndex]
> }
>
> async function orderedSwaps(trades: Iterable<Trade>): Promise<OrderedSwap[]> {
>   const swaps = await Promise.all([...trades].filter(isSwap).map(async trade => {
>     if (!trade.sqrtPriceAfter) throw new Error("Swap price not indexed yet; retry")
>     return { trade, order: await swapOrder(trade) }
>   }))
>   return swaps.sort((a, b) => compareOrder(a.order, b.order))
> }
> ```
>
> **Compute what each swap changed**
>
> The previous swap's ending price is the next swap's starting price. `printFills` compares inventory at those two prices and prints signed changes in token base units: positive means acquired, negative means released. The first read only establishes the starting price; if the pool has no history, its first swap establishes it instead.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> type LastSwap = { price: string; order: Order }
>
> function printFills(
>   position: Position, swaps: OrderedSwap[],
>   previous: LastSwap | undefined, starting: boolean,
> ) {
>   for (const item of swaps) {
>     if (previous && compareOrder(item.order, previous.order) <= 0) {
>       throw new Error("Late or ambiguous swap; restart the tracker")
>     }
>     if (previous && !starting) {
>       const before = inventoryAt(position, previous.price)
>       const after = inventoryAt(position, item.trade.sqrtPriceAfter!)
>       console.log({
>         tradeId: item.trade.id,
>         transactionId: item.trade.transactionHash,
>         delta0: (after.amount0 - before.amount0).toString(),
>         delta1: (after.amount1 - before.amount1).toString(),
>       })
>     }
>     previous = { price: item.trade.sqrtPriceAfter!, order: item.order }
>   }
>   return previous
> }
> ```
>
> **Run the tracker**
>
> `watchPositionFills` connects the functions and checks for new trades every five seconds. It remembers processed IDs to avoid printing a fill twice. Stop it before changing liquidity or rebalancing, then restart afterward with the current position ID.
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> async function watchPositionFills(positionTokenId: string) {
>   const position = await client.getPosition({ positionTokenId })
>   if (!position || position.liquidity === 0n) throw new Error("No active position")
>   const seen = new Set<string>()
>   let previous: LastSwap | undefined
>   let starting = true
>
>   while (true) {
>     const fresh = await readNewTrades(position.pool, seen, starting)
>     await checkPosition(positionTokenId, position)
>     const swaps = await orderedSwaps(fresh.values())
>     previous = printFills(position, swaps, previous, starting)
>     for (const id of fresh.keys()) seen.add(id)
>     starting = false
>     await new Promise(resolve => setTimeout(resolve, 5_000))
>   }
> }
>
> await watchPositionFills(POSITION_TOKEN_ID)
> ```

> [tab] Python
>
> Add these blocks to the same script, in order, using `dex` and `aleo` from Setup. Set `NODE` to a node on the same network; the URL below is mainnet.
>
> **Read the position**
>
> `read_position` loads the position's range and liquidity from the core contract. `check_position` stops the tracker if those values change, so a deposit or withdrawal is not counted as a swap.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> import time
> import requests
> from aleo.codegen.runtime import parse_plaintext
> from aleo_shield_swap.position_math import amounts_for_liquidity
> from aleo_shield_swap.tick_math import get_sqrt_price_at_tick_x128
>
> NODE = "https://edge.provable.com/api/v2/mainnet"
>
> def read_position(position_token_id):
>     raw = aleo.network.get_program_mapping_value(
>         "shield_swap.aleo", "positions", position_token_id,
>     )
>     return parse_plaintext(raw) if raw else None
>
> def check_position(position_token_id, original):
>     current = read_position(position_token_id)
>     fields = ("pool", "tick_lower", "tick_upper", "liquidity")
>     if not current or any(current[k] != original[k] for k in fields):
>         raise RuntimeError("Position changed; restart the tracker")
> ```
>
> **Calculate the tokens backing the position**
>
> `inventory_at` returns the position's token0 and token1 amounts at a given square-root price. We will call it before and after each swap to find the difference.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def inventory_at(position, price):
>     return amounts_for_liquidity(
>         int(price),
>         get_sqrt_price_at_tick_x128(position["tick_lower"]),
>         get_sqrt_price_at_tick_x128(position["tick_upper"]),
>         position["liquidity"],
>         round_up=False,
>     )
> ```
>
> **Fetch trades not yet seen**
>
> The API returns newest trades first. `read_new_trades` fetches pages until it reaches a known trade, then finishes reading every trade with that timestamp so none are skipped across pages. On startup, it reads the latest swap's full timestamp group to find which swap actually came last.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def is_swap(trade):
>     return trade.tradeType.value in ("swap", "swap_multi_hop")
>
> def read_new_trades(pool, seen, starting):
>     fresh, offset = {}, 0
>     overlap_at = None
>     while True:
>         rows = dex.api.get_pool_trades(pool, limit=100, offset=offset)
>         fresh.update((t.id, t) for t in rows if t.id not in seen)
>         if overlap_at is None:
>             overlap_at = next((t.executedAt for t in rows if t.id in seen), None)
>         if len(rows) < 100:
>             if seen and overlap_at is None:
>                 raise RuntimeError("History gap; restart the tracker")
>             break
>         if overlap_at is not None and rows[-1].executedAt != overlap_at:
>             break
>         latest_swap = next((t for t in fresh.values() if is_swap(t)), None)
>         if starting and latest_swap and rows[-1].executedAt != latest_swap.executedAt:
>             break
>         offset += 100
>     return fresh
> ```
>
> **Put swaps in execution order**
>
> Timestamps can tie. `swap_order` checks that the transaction was accepted and locates the swap by block, transaction, program call, and hop. `ordered_swaps` drops non-swap activity and sorts the remaining trades oldest first; it stops if a price or an unambiguous program call is missing.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def swap_order(trade):
>     response = requests.get(
>         f"{NODE}/find/blockHash/{trade.transactionHash}", timeout=30,
>     )
>     response.raise_for_status()
>     block = aleo.network.get_block_by_hash(response.json())
>     confirmed = next(t for t in block.get("transactions", [])
>                      if t["transaction"]["id"] == trade.transactionHash)
>     if confirmed["status"] != "accepted":
>         raise RuntimeError("Swap is not accepted")
>     transitions = confirmed["transaction"].get("execution", {}).get("transitions", [])
>     matches = [i for i, t in enumerate(transitions)
>                if t["program"] == "shield_swap.aleo" and t["function"] == trade.tradeType.value]
>     if len(matches) != 1:
>         raise RuntimeError("Cannot resolve swap transition uniquely")
>     return (block["header"]["metadata"]["height"], confirmed["index"],
>             matches[0], trade.legIndex)
>
> def ordered_swaps(trades):
>     swaps = []
>     for trade in trades:
>         if not is_swap(trade):
>             continue
>         if not trade.sqrtPriceAfter:
>             raise RuntimeError("Swap price not indexed yet; retry")
>         swaps.append((swap_order(trade), trade))
>     return sorted(swaps, key=lambda item: item[0])
> ```
>
> **Compute what each swap changed**
>
> The previous swap's ending price is the next swap's starting price. `print_fills` compares inventory at those two prices and prints signed changes in token base units: positive means acquired, negative means released. The first read only establishes the starting price; if the pool has no history, its first swap establishes it instead.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def print_fills(position, swaps, previous, starting):
>     for key, trade in swaps:
>         if previous and key <= previous[0]:
>             raise RuntimeError("Late or ambiguous swap; restart the tracker")
>         if previous and not starting:
>             before = inventory_at(position, previous[1])
>             after = inventory_at(position, trade.sqrtPriceAfter)
>             print({
>                 "tradeId": trade.id,
>                 "transactionId": trade.transactionHash,
>                 "delta0": after[0] - before[0],
>                 "delta1": after[1] - before[1],
>             })
>         previous = (key, trade.sqrtPriceAfter)
>     return previous
> ```
>
> **Run the tracker**
>
> `watch_position_fills` connects the functions and checks for new trades every five seconds. It remembers processed IDs to avoid printing a fill twice. Stop it before changing liquidity or rebalancing, then restart afterward with the current position ID.
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> def watch_position_fills(position_token_id):
>     position = read_position(position_token_id)
>     if not position or position["liquidity"] == 0:
>         raise RuntimeError("No active position")
>     seen, previous, starting = set(), None, True
>
>     while True:
>         fresh = read_new_trades(position["pool"], seen, starting)
>         check_position(position_token_id, position)
>         swaps = ordered_swaps(fresh.values())
>         previous = print_fills(position, swaps, previous, starting)
>         seen.update(fresh)
>         starting = False
>         time.sleep(5)
>
> watch_position_fills(POSITION_TOKEN_ID)
> ```

For historical replay and WebSocket updates, see the [complete runnable tracker](https://github.com/ProvableHQ/veil/blob/main/examples/shield-swap/lp-fill-tracker.ts).

## Calculate a fill

Compare the position's inventory immediately before and after a swap, using the range and liquidity active during that swap. The examples below use liquidity `1,000,000`, range ticks `[-100, 100)`, and a price move from tick `0` to tick `10`. They run locally without a key.

Prices use **Q128.128** integers; amounts use token base units. Install the latest Typescript SDK or Python SDK using [Setup](https://shield.fi/docs/trading/configure.md).

> [tab] Typescript
>
> ```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> import { amountsForLiquidity, getSqrtPriceAtTickX128 } from "@provablehq/shield-swap-sdk"
>
> const range = {
>   liquidity: 1_000_000n,
>   sqrtLowerX128: getSqrtPriceAtTickX128(-100),
>   sqrtUpperX128: getSqrtPriceAtTickX128(100),
>   roundUp: false,
> }
> const before = amountsForLiquidity({
>   ...range,
>   sqrtPriceX128: getSqrtPriceAtTickX128(0),
> })
> const after = amountsForLiquidity({
>   ...range,
>   sqrtPriceX128: getSqrtPriceAtTickX128(10),
> })
> console.log({
>   delta0: (after.amount0 - before.amount0).toString(),
>   delta1: (after.amount1 - before.amount1).toString(),
> })
> ```

> [tab] Python
>
> ```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
> from aleo_shield_swap.position_math import amounts_for_liquidity
> from aleo_shield_swap.tick_math import get_sqrt_price_at_tick_x128
>
> liquidity = 1_000_000
> lower = get_sqrt_price_at_tick_x128(-100)
> upper = get_sqrt_price_at_tick_x128(100)
> before = amounts_for_liquidity(
>     get_sqrt_price_at_tick_x128(0), lower, upper, liquidity, round_up=False,
> )
> after = amounts_for_liquidity(
>     get_sqrt_price_at_tick_x128(10), lower, upper, liquidity, round_up=False,
> )
> print({"delta0": after[0] - before[0], "delta1": after[1] - before[1]})
> ```

Each example reports **−500 token0** and **+500 token1**, in base units: the position released token0 and acquired token1. These are rounded inventory changes, not separate wallet payments or profit.

For live data:

* Use accepted [pool swaps](https://shield.fi/docs/api-reference/pools/list-pool-trades.md) in chain order, with each swap's actual before/after square-root prices. Rounded ticks or snapshots spanning several swaps cannot recover individual fills.
* Use the position state effective for each swap. Start after mint, update liquidity after increases or decreases, and track a rebalance's replacement under its new position ID.
* Keep values as integers or decimal strings. Track [fee income](https://shield.fi/docs/liquidity/fee-entitlements.md) separately.

See [Rebalancing positions](https://shield.fi/docs/liquidity/rebalance-position.md) for changing the range.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.