Skip to main content
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 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 = null are 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: true and applied_filters: { since, until }. These keys are absent from default responses.
  • Bad inputs return 400 with 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=nba matches markets whose tag_slugs array contains "nba").
  • Positions with tag_slugs = null are dropped under an active filter — same logic as last_trade_at for since/until (cannot match what we don’t know).
  • Aggregates recompute over the filtered set.
  • applied_filters.tag_slug echoes the value back so the response is self-documenting.
  • Combine with since / until to 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

This collapses across every children market under the FIFA World Cup winner 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:
The values returned by /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 0.40thatresolvesat0.40 that resolves at 1.00:
  • avg_price = 0.40
  • total_bought = 10,000 (tokens acquired)
  • Cost basis = 10,000 × 0.40=0.40 = 4,000 USDC
  • Payout at 1.00=10,000×1.00 = 10,000 × 1.00 = $10,000 USDC
  • realized_pnl = 10,00010,000 - 4,000 = $6,000
The formula: 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.
When you supply 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; passing since/until does not invalidate the underlying cache.

Examples

Default response (no filter)

Returns every position the wallet has held, all-time, with the full set of fields documented above. Default response shape — no 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:
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.

Authorizations

x-api-key
string
header
required

Path Parameters

address
string
required

Wallet address (0x-prefixed)

Response

Onchain positions with P&L