/v4/analytics/positions/{userAddress}
Get user position historyFetch historical position data for a user across opportunities.
Auth: a valid x-api-key developer key, or an authenticated JWT identity (cookie or Bearer) — both grant the same read access.
Response:
- data[chainId][identifier][indexingId]: Wrapped positions grouped by chain ID → identifier → indexingId. Each positionGroup carries shared metadata (protocol, action, stateType, globalStates, tokens) and a positions map keyed by positionId.
- 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 (populated per snapshot when includeDisplay=true): at the positionGroup level it holds series descriptors only (type, indexingId). The value applied to a snapshot lives on that snapshot's own globalStates[key] = { blockTimestamp, value } — the series row at-or-before the snapshot's timestamp (carry-forward) — so each snapshot (latest + each history entry) is self-describing.
- 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: Number of positions returned
- pagination.totalHistorySnapshots: Total history snapshots (when > 0)
- pagination.pageInfo: Cursor-based pagination info (when applicable)
PnL (includePnl=true): adds a running pnl block to every snapshot's display — cost basis, per-event cashflow, PnL, ROI, all USD at each event's own daily-close price — A position's own PnL is its latest.display.pnl; there is no duplicate block on the position.
Every history row is one capital event, so a row's state delta is the cashflow and everything else is PnL. PnL therefore needs the complete series: the flag implies includeDisplay and includeHistory, and disregards first/after, since first pages rows newest-first portfolio-wide and drops the oldest writes — exactly the cost basis. The walk is row-capped; if it stops early, or a minTimestamp anchor opens the series, the block reports basis: "truncated". Scope large portfolios with identifier or indexingIds.
summary carries the request's aggregates: total plus byIdentifier nested chainId → identifier, mirroring data. ROI is recomputed from the aggregate, never averaged, and divides by mean capital at risk (the basis time-weighted over the time it was funded) — so it survives a position closing, where the net basis goes negative, and it is not inflated by capital recycled through the position. The per-position means are summed, which is exact only where the positions' windows coincide. A borrow's cost basis is capital *received*, so costBasisUsd subtracts it and reports net capital committed. Always read positionsIncluded/positionsExcluded — PnL cannot be computed for every position (unsupported stateType, unpriced token), and a total that omits some says so there rather than looking complete.
Excluded: protocol fees (CLAMM state carries liquidity only) and Merkl incentives (not position states).
Examples:
- All positions on chain 1: GET /v4/analytics/positions/0xUserAddress/1
- With history: GET /v4/analytics/positions/0xUserAddress/1?includeHistory=true
- With USD display values: GET /v4/analytics/positions/0xUserAddress/1?includeDisplay=true
- With PnL: GET /v4/analytics/positions/0xUserAddress/1?includePnl=true&identifier=0xPoolAddress
- Filter by protocol: GET /v4/analytics/positions/0xUserAddress/1?protocol=uniswap
- Filter by identifier: GET /v4/analytics/positions/0xUserAddress/1?identifier=0xPoolAddress
Path parameters
userAddressstringchainIdnumber | stringQuery parameters
includeHistorybooleanincludeDisplaybooleanincludePnlbooleanminTimestampnumber | stringdailybooleanfirstnumber | stringdefault: 1000protocolstringidentifierstringindexingIdsstring[]afterstringResponses
200Response for status 200
Schema
object
dataobjectsummaryobjectPnL aggregates. Present only when `includePnl=true`.
totalobjectrequired
costBasisUsdnumberNet cashflow through the position, in its own direction: capital invested for a supply, principal outstanding for a borrow. Goes negative once outflows exceed inflows — on a closed position that surplus is the profit, so `pnlUsd` is exactly its negation. A borrow's basis therefore stays positive, and the negation the summary applies to it is what makes its total read as net capital committed.pnlUsdnumberCurrent value minus `costBasisUsd`.roinumber`pnlUsd` over **mean capital at risk**: the cost basis integrated over the time it was positive, divided by that time. Not gross inflows, which count recycled capital once per round trip; not `costBasisUsd`, which inverts the ratio once a position closes. Idle stretches are excluded, so $100 held for a day then flat for a year is a return on $100. Percentage (15.5 = +15.5%). Absent when no capital was ever at risk. A mean, so it ignores when in the window the flows landed — not an IRR — and it does not net leverage off. Covers only the stretch since the basis last restarted: an unreadable row discards the basis it built, so on a restarted series — including one still reporting `complete` — this is the return over the post-restart tail. A full exit is not a restart.basisenumcompletetruncatedunpricedpositionsIncludednumberpositionsExcludednumberbyIdentifierobjectpaginationobjectrequired
positionCountnumbertotalHistorySnapshotsnumberTotal number of history snapshotspageInfoobject
endCursorstringCursor for the next pagehasNextPagebooleanhasMoreHistorybooleanExample
{
"data": {},
"summary": {
"total": {
"costBasisUsd": 0,
"pnlUsd": 0,
"roi": 0,
"basis": "complete",
"positionsIncluded": 0,
"positionsExcluded": 0
},
"byIdentifier": {}
},
"pagination": {
"positionCount": 0,
"totalHistorySnapshots": 0,
"pageInfo": {
"endCursor": "string",
"hasNextPage": false,
"hasMoreHistory": false
}
}
}https://analytics.merkl.xyz/v4/analytics/positions/{userAddress}curl -X GET "https://analytics.merkl.xyz/v4/analytics/positions/{userAddress}"