Onchain positions & P&L
Onchain V2 (legacy)
Positions & P&L (Wallet)
Every position a wallet has held — open and closed — with realized + unrealized P&L, per-position activity timestamps, market resolution timestamps, and parent event slugs. Optional time-window filtering.
GET
Onchain positions & P&L
Returns every position a wallet has ever held, including closed positions that no longer appear in the standard positions endpoint. Each position includes precomputed
This collapses across every children market under the FIFA World Cup winner event.
The values returned by How
Each position’s
When you supply
Returns every position the wallet has held, all-time, with the full set of fields documented above. Default response shape — no
This collapses a wallet’s positions across the children of an event (e.g. all 32 World Cup winner markets, or the multiple lines of a single NBA game) into one row per event.
realized_pnl, avg_price, total_bought, plus per-position activity, redemption, and resolution timestamps that let you build leaderboards, trim to a 30-day or 90-day window, or filter to a parent event.
The standard /v1/wallets/{addr}/positions endpoint only returns open positions. Once a position is fully exited or a market resolves, it disappears. This endpoint fills that gap.
Request
since, until, and tag_slug are all opt-in. When none are supplied, the response is identical to a request with no parameters at all — same shape, same aggregates over the full position set. When any is supplied, positions are filtered and aggregates are recomputed (see Filtering).
Response
Distinguishing the three timestamp fields
These three timestamp fields look similar but answer different questions. Choosing the right one matters when filtering:
A position can have
resolved_at populated and closed_at null — the market resolved but the wallet has not yet redeemed. A position can have closed_at set and resolved_at null — the wallet redeemed during a window polynode does not have full historical resolution coverage for, but the redemption itself was observed.
For windowing a leaderboard to “the last 30 days of trading activity,” last_trade_at is the right field — it captures every position the wallet was active in. For “positions resolved in the last 90 days,” resolved_at is the right one. For “positions the wallet has actually closed out,” closed_at is the right one.
Filtering by activity window
since and until are optional unix-seconds query parameters that filter positions by last_trade_at. Designed to support 30d / 90d / 365d leaderboard windows without fetching multi-year history just to trim it client-side.
Behavior
- Both bounds are inclusive.
- Positions with
last_trade_at = nullare dropped under any active filter — there’s no timestamp to compare against. - All seven aggregates (
count,open_count,closed_count,total_realized_pnl,total_unrealized_pnl,total_pnl,positions_with_pnl) recompute over the filtered set. - The response gains two top-level keys to make the shift self-documenting:
filtered: trueandapplied_filters: { since, until }. These keys are absent from default responses. - Bad inputs return
400with a descriptive error.
30-day window
90-day window
Bounded range (e.g. Q1 2026)
The default response is unaffected by these new params. When you call this endpoint with no
since/until/tag_slug, the response shape and every aggregate is identical to what you got before. The filtered and applied_filters keys are only added when you opt in.Filtering by tag
tag_slug is an optional query parameter that keeps only positions whose market carries the matching Polymarket event tag.
Tags come from Polymarket’s event taxonomy — typical values include nba, basketball, sports, politics, crypto, fed, world, 2025-predictions, and event-specific slugs like 2026-fifa-world-cup-winner-595. Each market inherits the tag set of its parent event, so all 32 markets under “Will X win the World Cup?” share the same tag list. The full set is visible in the per-row tag_slugs array on every response.
Behavior
- Match is case-sensitive and exact (
?tag_slug=nbamatches markets whosetag_slugsarray contains"nba"). - Positions with
tag_slugs = nullare dropped under an active filter — same logic aslast_trade_atforsince/until(cannot match what we don’t know). - Aggregates recompute over the filtered set.
applied_filters.tag_slugechoes the value back so the response is self-documenting.- Combine with
since/untilto get e.g. “all NBA positions in the last 30 days” — all three filters AND together.
Examples
All NBA positions for a wallet
All positions tagged to a specific event
Crypto positions in the last 90 days
Discovering available tags
Use the dedicated/v2/onchain/tags endpoint for the full tag namespace (5,779 tags currently). Returns a flat slug array by default, or ?details=true for per-tag market counts and timestamps:
/v2/onchain/tags are the exact strings to pass to ?tag_slug= here.
For a wallet-specific view, you can also fetch the wallet’s positions without a filter and read the union of tag_slugs across the response — that’s the tag namespace the wallet has actually traded in.
How realized_pnl Works
Each position’s realized_pnl reflects the profit or loss after the position is closed (sold or redeemed). For a winning binary position bought at 1.00:
avg_price= 0.40total_bought= 10,000 (tokens acquired)- Cost basis = 10,000 × 4,000 USDC
- Payout at 1.00 = $10,000 USDC
realized_pnl= 4,000 = $6,000
realized_pnl = total_bought × (1 - avg_price) for positions that resolve at $1.
Per-position values (realized_pnl, avg_price, total_bought) come from the same onchain settlement data that Polymarket uses. Individual position P&L matches what Polymarket shows on its portfolio page.
Which total to use
The response returns three portfolio-level totals. Each answers a different question:total_pnl is what most dashboards want — it’s the single number users recognize from Polymarket. total_realized_pnl is useful for attribution (which markets made money vs which positions are still out). total_unrealized_pnl is useful for risk views (how much is still at stake).
total_pnl = total_realized_pnl + total_unrealized_pnl. Reach for total_pnl when you want a number that matches a Polymarket profile page. Reach for total_realized_pnl when you want only closed-out gains.since or until, all three totals recompute over the filtered set — the same logic, just narrower input.
Performance
Responses are cached for 5 minutes per wallet. First request for a new wallet takes 200ms-3s depending on position count (up to 20,000 positions supported). Cached responses return in under 50ms. Filter parameters are applied to the cached set; passingsince/until does not invalidate the underlying cache.
Examples
Default response (no filter)
filtered or applied_filters keys.
Filter to the last 30 days
Filter to a parent event
event_slug is included on every row, so once you have a wallet’s positions you can group them by parent event without an extra round-trip:

