Merkl Developer Portal logo
get

/v4/analytics/positions/by-identifier/{chainId}/{identifier}

Get positions by opportunity identifier

Fetch the latest position for every user in a given opportunity. indexingId reads one series only. With indexingId, first returns only the first positions of that series, in position-key order, not by amount. Without first, each series returns at most its first 50000 positions. A series with more positions than this limit carries truncated: true.

Past instant. at=<timestamp> (with indexingId, not with first) returns each position as it stood then: its last state at or before at, with display and stats valued at that instant (block, daily close and global states at at). The response keeps the same shape.

One holder. positionKey=<key> (with indexingId) returns only that key's positions — the holder address for most series. It combines with at. Add positionGranularityKey=<key> to read only one sub-position of that key (e.g. one tick range); without it, every sub-position.

Stats. includeStats=true adds stats over the returned positions. It requires indexingId and implies includeDisplay. Both blocks read each position's display: stats.amount sums its amount (with that token's decimals) and is present only when every position has one amount, all in one token; stats.valueUsd sums its valueUsd and is present only when every position has one. Each block gives holders, maxPositionsPerHolder, total, average, median, max, largest (address, share), whales (holders with at least 1%) and concentration (top 1/10/100, holders for 50%/90%, share tiers at 1%/0.1%/0.01%, gini). stats.positions counts the positions: total, open, empty, withoutAmount, and estimated (valued at an estimated price).

Response:
- data[indexingId]: Wrapped positions keyed by indexingId. Each positionGroup carries shared metadata (protocol, name, action, stateType, globalStates, tokens) and a positions map keyed by positionId. Each position entry carries its user field; multiple users may share the same indexingId positionGroup.
- latest is reported as of the indexing checkpoint: its (blockNumber, blockTimestamp) are advanced to how far indexing has completed (state unchanged through there), never beyond it — so the state is never presented as more current than what's indexed.
- globalStates: at the positionGroup level it holds series descriptors only (type, indexingId) — fetch the values from GET /v4/analytics/global-states. This route returns raw states; it runs no global-state enrichment.
- display.unpricedReason (when valueUsd is absent): which step gave up — no_price (The amount maps to its token(s), but one has no daily close at or before that day — a price-coverage gap, not a shape the valuation misses.), no_token_mapped (No pricing token: no declared token denominates the amount, which is what a market-keyed identifier (Morpho, Euler) looks like.), unattributable_leg (A leg of a two-legged position maps to no single token (a smart collateral / smart debt pair, or an unlisted token), so the net value would be partial.), missing_global_state (The enriching global series has no row at or before the snapshot — the snapshot predates the series, so no amount resolves.), unsupported_processor (The processor is unknown, or the states-resolver has no extractor for it — an integration gap rather than a price one.), missing_position_params (The position carries no tick range, so its CLAMM amounts cannot be computed.), unresolved_amount (Global-state enrichment produced no resolved amount (a state missing a leg or a multiplier), or none ran.), unsupported_state (The stateType has no valuation rule, or the state is missing its amount — an integration gap rather than a price one.). A price gap and an unsupported shape both read as "no value" without it.
- display.amounts[].priceUsd: the daily close the entry was valued at, present whenever the token priced — also on a zero amount, where valueUsd / amount cannot recover it. It is the last close at or before the snapshot's day, so it can carry forward.
- Estimated price. A token with no close at or before the snapshot's day takes its first close, if at most 30 days later. The entry then carries priceEstimatedAt, and display.priceEstimated (or legs[].priceEstimated) marks valueUsd as an estimate. PnL never uses an estimate.
- display.legs (two-legged positions, e.g. Fluid dual_balance): one entry per declared leg (side, debt on the borrow leg, amounts, unsigned valueUsd or unpricedReason). display.valueUsd is then the net value, collateral minus debt, and is present only when every leg priced.
- debt: present and true when the group's amounts are owed, not held (a borrow) — sum values across groups by subtracting these. Set once per group: it follows from the processor.
- positions[positionId].params: position-invariant fields hoisted out of the snapshots, emitted once per position. Shape follows stateType; CLAMM carries tickLower/tickUpper (and salt when the persisted state has one), which is why they are no longer inside each snapshot's state. poolId and tokenId are not emitted at all: poolId is the positionGroup's identifier, and tokenId is the last segment of the positionId key.
- pagination.positionCount: Total number of position entries returned
- pagination.recipientsCount: Number of distinct user addresses across all positions

- Confidential sets are withheld. A storing set whose state is a decrypted confidential amount (Zama ERC-7984 plaintext balances) is not served here: the chain publishes the ciphertext only, and the plaintext exists because the holder delegated decryption to Merkl. The public sibling series (confidential_transfer ciphertext handles, delegation ACL expiries) are returned as usual.

Examples:
- Latest positions: GET /v4/analytics/positions/by-identifier/1/0xPoolAddress
- One holder at a past instant: GET /v4/analytics/positions/by-identifier/1/0xPoolAddress?indexingId=<indexingId>&at=1748635620&positionKey=0xUser

Path parameters

chainIdstring (integer) | integer
required

Chain ID to query

identifierstring
required

Pool address, token address, market ID, or any opportunity identifier

Query parameters

includeDisplayboolean
required
default: false

Decorate each snapshot with the same self-describing display block as the by-user route (per-token id/symbol/decimals/amount plus valueUsd). Enriches the states it needs on its own.

indexingIdstring

Read only this series, without the identifier lookup.

includeStatsboolean

Add stats over the returned positions. Requires indexingId. Implies includeDisplay, so valueUsd figures come from the same valuation.

atnumber | string

Unix timestamp (s). Return each position as it stood then (its last state at or before at), valued at that instant. Requires indexingId; not with first.

positionKeystring

Return only the positions of this position key (the holder address for most series). Requires indexingId.

positionGranularityKeystring

Return only this sub-position of positionKey (e.g. one tick range or market). Without it, every sub-position of the key. Requires positionKey.

firststring (integer) | integer

Return only the first first positions of the series, in position-key order (not by amount). Without it, the first 50000 positions of each series. Requires indexingId.

Responses

200
Response for status 200
Schema
object
dataobject
required
Wrapped positions keyed by indexingId
paginationobject
required
positionCountnumber
required
Total number of position entries returned
recipientsCountnumber
required
Number of distinct user addresses in the response
Example
{
  "data": {},
  "pagination": {
    "positionCount": 0,
    "recipientsCount": 0
  }
}