get

/v4/analytics/positions/{userAddress}

Get user position history

Fetch 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

userAddressstring
required
chainIdnumber | string
required

Query parameters

includeHistoryboolean
required
default: false
includeDisplayboolean
required
default: false
includePnlboolean
required
default: false
minTimestampnumber | string
dailyboolean
required
default: false
firstnumber | stringdefault: 1000
protocolstring
identifierstring
indexingIdsstring[]
afterstring

Responses

200
Response for status 200
Schema
object
dataobject
required
Wrapped positions keyed by chainId, then identifier, then indexingId
summaryobjectPnL aggregates. Present only when `includePnl=true`.
totalobject
required
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.
basisenum
required
completetruncatedunpriced
positionsIncludednumber
required
Positions whose figures are in these totals.
positionsExcludednumber
required
Positions left out (no cashflow source for the stateType, or nothing priced).
byIdentifierobject
required
Keyed by chainId, then identifier — the same nesting as `data`.
paginationobject
required
positionCountnumber
required
Actual number of items returned
totalHistorySnapshotsnumberTotal number of history snapshots
pageInfoobject
endCursorstringCursor for the next page
hasNextPageboolean
required
Whether more results are available — matches the transactions route
hasMoreHistoryboolean
required
Deprecated alias of `hasNextPage`, kept for existing consumers. Prefer `hasNextPage`.
Example
{
  "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
    }
  }
}
get
https://analytics.merkl.xyz/v4/analytics/positions/{userAddress}
Merkl Analytics API
curl -X GET "https://analytics.merkl.xyz/v4/analytics/positions/{userAddress}"
Fill the required path parameters userAddress, chainId to send.