> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

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

```
GET /v2/wallets/{address}/positions/onchain
```

| Parameter  | Type             | Description                                                                                                                                                                                                                           |
| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`  | path             | Wallet address (0x-prefixed, 40 hex chars)                                                                                                                                                                                            |
| `since`    | query (optional) | Unix seconds. Drop positions whose `last_trade_at` is before this timestamp.                                                                                                                                                          |
| `until`    | query (optional) | Unix seconds. Drop positions whose `last_trade_at` is after this timestamp.                                                                                                                                                           |
| `tag_slug` | query (optional) | Keep only positions whose market carries this Polymarket event tag (e.g. `nba`, `politics`, `crypto`, `2026-fifa-world-cup-winner-595`). Matches against the per-market `tag_slugs` array. See [Filtering by tag](#filtering-by-tag). |

`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](#filtering-by-activity-window)).

## Response

```json theme={null}
{
  "wallet": "0xbddf61af533ff524d27154e589d2d7a81510c684",
  "source": "onchain",
  "count": 879,
  "open_count": 340,
  "closed_count": 288,
  "total_realized_pnl": 17183579.48,
  "total_unrealized_pnl": -14346997.32,
  "total_pnl": 2836582.16,
  "positions_with_pnl": 309,
  "positions": [
    {
      "token_id": "100424505911492726143069668369640898890012859227903329249368753635028896002613",
      "size": 0,
      "avg_price": 0.63,
      "realized_pnl": 147588.53019,
      "unrealized_pnl": 0,
      "current_price": null,
      "market_status": "closed",
      "total_bought": 398887.919433,
      "market": "Will Fulham FC win on 2026-03-21?",
      "slug": "epl-ful-bur-2026-03-21-ful",
      "event_slug": "epl-ful-bur-2026-03-21",
      "outcome": "Yes",
      "image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/Repetitive-markets/premier+league.jpg",
      "condition_id": "0xae68634cb01137acbe26e65790f7268a523ee1519601f2576bd7b067c7c9f9da",
      "last_trade_at": 1742568134,
      "closed_at": 1742573012,
      "resolved_at": 1742572840
    }
  ]
}
```

| Field                        | Type              | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wallet`                     | string            | Queried wallet address (lowercased)                                                                                                                                                                                                                                                                                                                                                                                         |
| `source`                     | string            | Always `"onchain"`                                                                                                                                                                                                                                                                                                                                                                                                          |
| `count`                      | number            | Total positions returned                                                                                                                                                                                                                                                                                                                                                                                                    |
| `open_count`                 | number            | Positions with `size > 0`                                                                                                                                                                                                                                                                                                                                                                                                   |
| `closed_count`               | number            | Positions with `size = 0` and nonzero `realized_pnl`                                                                                                                                                                                                                                                                                                                                                                        |
| `total_realized_pnl`         | number            | Sum of `realized_pnl` across all returned positions                                                                                                                                                                                                                                                                                                                                                                         |
| `total_unrealized_pnl`       | number            | Sum of `unrealized_pnl` across all returned positions. Reflects open-size paper P\&L at current (or terminal) market prices.                                                                                                                                                                                                                                                                                                |
| `total_pnl`                  | number            | `total_realized_pnl + total_unrealized_pnl`. Matches the net portfolio PnL shown on a Polymarket profile page.                                                                                                                                                                                                                                                                                                              |
| `positions_with_pnl`         | number            | Positions where `realized_pnl != 0`                                                                                                                                                                                                                                                                                                                                                                                         |
| `filtered`                   | boolean           | Present **only** when `since` or `until` was supplied. Always `true` in that case. Absent in default responses.                                                                                                                                                                                                                                                                                                             |
| `applied_filters`            | object            | Present **only** when filtering. Echoes `{since, until}` (each is the supplied unix seconds or `null`). Absent in default responses.                                                                                                                                                                                                                                                                                        |
| `positions[].token_id`       | string            | CTF token ID (called `asset` on the [closed-positions](/api-reference/wallets/closed-positions) endpoint)                                                                                                                                                                                                                                                                                                                   |
| `positions[].size`           | number            | Current token balance (0 = fully exited)                                                                                                                                                                                                                                                                                                                                                                                    |
| `positions[].avg_price`      | number            | Volume-weighted average entry price                                                                                                                                                                                                                                                                                                                                                                                         |
| `positions[].realized_pnl`   | number            | Realized profit/loss in USDC                                                                                                                                                                                                                                                                                                                                                                                                |
| `positions[].unrealized_pnl` | number            | Paper P\&L on remaining open shares. For resolved markets, uses the terminal settlement price (1.0 winner / 0.0 loser). For live markets, uses the current market price. `0` when `size = 0`.                                                                                                                                                                                                                               |
| `positions[].current_price`  | number \| null    | Price used for `unrealized_pnl`. `1.0` or `0.0` on resolved markets, live market price on active markets, `null` when `size = 0` or no price is available.                                                                                                                                                                                                                                                                  |
| `positions[].market_status`  | string            | `"live"` / `"resolved-win"` / `"resolved-loss"` / `"resolved-unknown"` / `"closed"`. See [positions feed](/api-reference/onchain/positions#position-fields) for full enum.                                                                                                                                                                                                                                                  |
| `positions[].total_bought`   | number            | Total tokens acquired for this position                                                                                                                                                                                                                                                                                                                                                                                     |
| `positions[].market`         | string            | Market question                                                                                                                                                                                                                                                                                                                                                                                                             |
| `positions[].slug`           | string            | **Market** slug — identifies the specific market within an event (e.g. `nba-bos-cle-2026-03-08-spread`).                                                                                                                                                                                                                                                                                                                    |
| `positions[].event_slug`     | string \| null    | **Event** slug — identifies the parent event a market belongs to (e.g. `nba-bos-cle-2026-03-08`). For single-market events, equals `slug`. For multi-market events (NBA games with several lines, election markets with several candidates, FIFA World Cup with one market per team, etc.) this is the parent the markets share. `null` when the event metadata is not yet known.                                           |
| `positions[].outcome`        | string            | Outcome label (e.g. "Yes", "No", "Up")                                                                                                                                                                                                                                                                                                                                                                                      |
| `positions[].image`          | string            | Market image URL                                                                                                                                                                                                                                                                                                                                                                                                            |
| `positions[].condition_id`   | string            | Market condition ID                                                                                                                                                                                                                                                                                                                                                                                                         |
| `positions[].last_trade_at`  | number \| null    | Unix seconds. Latest fill (maker or taker) on this token across V1 + V2 exchanges. `null` when polynode has no fill history for the token (e.g. positions acquired purely via splits/merges or NegRisk conversion with no later trade).                                                                                                                                                                                     |
| `positions[].closed_at`      | number \| null    | Unix seconds. Latest moment this wallet redeemed any outcome of this market's `condition_id` for collateral. `null` when the wallet has not redeemed (positions sold to zero before resolution, or open positions, or wins held but not yet redeemed).                                                                                                                                                                      |
| `positions[].resolved_at`    | number \| null    | Unix seconds. Moment the market was resolved on-chain (when payouts became redeemable). `null` for markets that resolved before polynode began tracking, or for markets that have not yet resolved. Recent markets are fully covered.                                                                                                                                                                                       |
| `positions[].tag_slugs`      | string\[] \| null | Polymarket event-level tags applied to this market. Examples: `["NBA","Basketball","Sports","All"]` for an NBA game market, `["Crypto","Up or Down","Recurring","All"]` for a 5-min crypto market. Order is most-specific → most-general. Tags live on the parent event (markets sharing an `event_slug` share the same `tag_slugs`). `null` only on the rare event that doesn't carry any tag or hasn't been ingested yet. |

## Distinguishing the three timestamp fields

These three timestamp fields look similar but answer different questions. Choosing the right one matters when filtering:

| Field           | Whose action                                                      | When                                                            |
| --------------- | ----------------------------------------------------------------- | --------------------------------------------------------------- |
| `last_trade_at` | The **wallet**'s most recent fill on this exact token             | Each fill event (buy or sell) updates this                      |
| `closed_at`     | The **wallet**'s most recent collateral redemption for the market | Set when the wallet calls `redeemPositions` on the CTF contract |
| `resolved_at`   | The **market itself** (not the wallet) becoming redeemable        | Set once per market, when the oracle reports payouts on-chain   |

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

```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
  -H "x-api-key: YOUR_KEY"
```

### 90-day window

```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 90 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
  -H "x-api-key: YOUR_KEY"
```

### Bounded range (e.g. Q1 2026)

```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?since=1735689600&until=1743465600" \
  -H "x-api-key: YOUR_KEY"
```

<Note>
  **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.
</Note>

## 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

```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=nba" \
  -H "x-api-key: YOUR_KEY"
```

#### All positions tagged to a specific event

```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=2026-fifa-world-cup-winner-595" \
  -H "x-api-key: YOUR_KEY"
```

This collapses across every children market under the FIFA World Cup winner event.

#### Crypto positions in the last 90 days

```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 90 ))
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain?tag_slug=crypto&since=$SINCE" \
  -H "x-api-key: YOUR_KEY"
```

### Discovering available tags

Use the dedicated [`/v2/onchain/tags`](/api-reference/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:

```bash theme={null}
curl "https://api.polynode.dev/v2/onchain/tags?min_markets=100&details=true" \
  -H "x-api-key: YOUR_KEY"
```

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.40 that resolves at $1.00:

* `avg_price` = 0.40
* `total_bought` = 10,000 (tokens acquired)
* Cost basis = 10,000 × $0.40 = $4,000 USDC
* Payout at $1.00 = 10,000 × $1.00 = \$10,000 USDC
* `realized_pnl` = $10,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                  | Answers                                                                                        | Matches                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `total_realized_pnl`   | How much profit has this wallet actually taken off the table from closed + resolved positions? | Not shown on Polymarket profile                  |
| `total_unrealized_pnl` | What is the paper P\&L on the wallet's currently-held shares at current (or terminal) prices?  | Not shown directly                               |
| `total_pnl`            | What is the wallet's net portfolio P\&L right now?                                             | The "Profit" number on a Polymarket profile page |

`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).

<Note>
  `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.
</Note>

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.

| Positions   | First request | Cached  |
| ----------- | ------------- | ------- |
| \< 500      | \~1s          | \< 50ms |
| 500-1,000   | \~1.5s        | \< 50ms |
| 1,000-5,000 | \~3s          | \< 50ms |

## Examples

### Default response (no filter)

```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain" \
  -H "x-api-key: YOUR_KEY"
```

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

```bash theme={null}
SINCE=$(( $(date -u +%s) - 86400 * 30 ))
curl "https://api.polynode.dev/v2/wallets/0xbddf61af533ff524d27154e589d2d7a81510c684/positions/onchain?since=$SINCE" \
  -H "x-api-key: YOUR_KEY"
```

### 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:

```bash theme={null}
curl "https://api.polynode.dev/v2/wallets/0x.../positions/onchain" \
  -H "x-api-key: YOUR_KEY" \
| jq '.positions | group_by(.event_slug) | map({event_slug: .[0].event_slug, market_count: length, pnl: (map(.realized_pnl + .unrealized_pnl) | add)})'
```

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.


## OpenAPI

````yaml GET /v2/wallets/{address}/positions/onchain
openapi: 3.1.0
info:
  title: PolyNode API
  description: >-
    Real-time Polymarket data API with decoded mempool settlements, OHLCV
    candles, and full Polygon JSON-RPC proxy.
  contact:
    name: PolyNode
    url: https://polynode.dev
  license:
    name: ''
  version: 2.0.0
servers:
  - url: https://api.polynode.dev
    description: Production
security:
  - api_key: []
paths:
  /v2/wallets/{address}/positions/onchain:
    get:
      tags:
        - Wallets
      summary: Onchain positions & P&L
      description: >-
        Returns all positions (open and closed) for a wallet with accurate
        realized P&L from onchain settlement data. Includes positions that no
        longer appear in the standard v1 positions endpoint.


        Responses are cached for 5 minutes per wallet. First request takes
        200ms-3s depending on position count. Cached responses return in under
        50ms.
      operationId: get_wallet_onchain_positions
      parameters:
        - name: address
          in: path
          description: Wallet address (0x-prefixed)
          required: true
          schema:
            type: string
          example: '0xbddf61af533ff524d27154e589d2d7a81510c684'
      responses:
        '200':
          description: Onchain positions with P&L
          content:
            application/json:
              example:
                wallet: '0xbddf61af533ff524d27154e589d2d7a81510c684'
                source: onchain
                count: 873
                open_count: 334
                closed_count: 288
                total_realized_pnl: 17183579.48
                positions_with_pnl: 309
                positions:
                  - token_id: >-
                      34158857981196154020624191931838064546543114916900689904269497497174344049047
                    size: 0
                    avg_price: 0.316481
                    realized_pnl: 447182.949852
                    total_bought: 654236.312162
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              example:
                error: API key required. Get one at polynode.dev
        '404':
          description: Wallet not found
          content:
            application/json:
              example:
                error: Not found.
components:
  securitySchemes:
    api_key:
      type: apiKey
      in: header
      name: x-api-key

````