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

# Backtest Copy PnL

> Score a wallet for copy-trading quality. Returns realized PnL, cashflow PnL, simulated copier PnL with 2% slippage friction, and a toxic-for-copying flag.

```http theme={null}
GET https://api.polynode.dev/v2/copy-pnl/{wallet}
```

## TL;DR — which PnL field do you actually want?

This endpoint returns **two different PnL numbers** that can look very different for the same wallet. Pick the right one for your use case:

| Field                     | What it answers                                                                | When to use                                                                                                                                                                            |
| ------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `total_realized_pnl_usdc` | "Did this wallet **make money** trading?"                                      | **Default for customer-facing UI.** Closest to what other trading platforms call "PnL". Equivalent to summing per-position WAVG-realized PnL across every position the wallet touched. |
| `actual_pnl_usdc`         | "How much **net cash** moved through this wallet's USDC balance from trading?" | Diagnostic / slippage-analysis. Used for `slippage_*` fields. Inflated for active wallets that hold inventory.                                                                         |

**Both numbers are correct** — they measure different things. See [the worked example below](#understanding-cashflow-pnl-vs-realized-pnl) for the math.

## Path parameters

<ParamField path="wallet" type="string" required>
  The Polymarket wallet address (Safe proxy or EOA, normalized to lowercase).
</ParamField>

## Query parameters

<ParamField query="period" type="string" default="30d">
  Convenience window preset. One of: `7d`, `14d`, `30d`, `60d`, `90d`, `180d`. Anchored to "now". If you pass `period`, you don't need `from`.
</ParamField>

<ParamField query="from" type="string | number">
  Start of the window. Accepts `YYYY-MM-DD` (UTC midnight) or unix seconds. Overrides `period` if both are passed.
</ParamField>

<ParamField query="to" type="string | number">
  End of the window. Same format as `from`. Defaults to current time when omitted.
</ParamField>

<ParamField query="include_trades" type="boolean" default="false">
  When `1` or `true`, response includes a `trades` array with one entry per fill (`ts`, `side`, `price`, `shares`, `actual_usd`, `backtest_usd`). Useful for transparency and debugging.
</ParamField>

## Response fields

| Field                         | Type           | Description                                                                                                                                                                                                                                               |          |       |          |                 |          |           |
| ----------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----- | -------- | --------------- | -------- | --------- |
| `wallet`                      | string         | Echo of the requested wallet (lowercased)                                                                                                                                                                                                                 |          |       |          |                 |          |           |
| **`total_realized_pnl_usdc`** | **number**     | **Realized PnL: signed sum of per-position WAVG-realized P/L across every position the matcher touched in the window. Counts only profit/loss on positions the wallet actually closed (or partially closed). Recommended primary "PnL" for customer UI.** |          |       |          |                 |          |           |
| `actual_pnl_usdc`             | number         | **Cashflow PnL** over the window: `-buys + sells + settlements`. Counts every USDC inflow/outflow from trading. Includes inventory bought but not yet sold (so often very negative for active buyers). Used as input to slippage formula.                 |          |       |          |                 |          |           |
| `backtest_copy_pnl_usdc`      | number         | Same cashflow walk with 2% slippage applied to buys (capped at \$1) and sells. Settlements unchanged.                                                                                                                                                     |          |       |          |                 |          |           |
| `slippage_amount_usdc`        | number         | `actual_pnl_usdc − backtest_copy_pnl_usdc`. The dollar friction the copier eats.                                                                                                                                                                          |          |       |          |                 |          |           |
| `slippage_cost_rate_pct`      | number \| null | `slippage / abs(actual_pnl) × 100`. `null` when `abs(actual_pnl) < 1` (denominator unstable).                                                                                                                                                             |          |       |          |                 |          |           |
| `toxic_for_copying`           | boolean        | `true` when `slippage_cost_rate_pct > 15`.                                                                                                                                                                                                                |          |       |          |                 |          |           |
| `trade_count`                 | int            | Number of fills walked in the window.                                                                                                                                                                                                                     |          |       |          |                 |          |           |
| `positions_closed`            | int            | Count of positions that contributed non-zero realized PnL in the window.                                                                                                                                                                                  |          |       |          |                 |          |           |
| `avg_entry_prob_weighted`     | number \| null | PnL-weighted average entry probability across closed positions. \`Σ(entry\_price ×                                                                                                                                                                        | realized | ) / Σ | realized | `. Null when `Σ | realized | \< \$1\`. |
| `avg_hold_seconds_weighted`   | number \| null | PnL-weighted average hold duration. Currently always `null` (WAVG matcher doesn't track per-segment timestamps; planned for future).                                                                                                                      |          |       |          |                 |          |           |
| `pnl_definition`              | string         | Always `"cashflow"` for `actual_pnl_usdc`. Reminds callers `actual_pnl_usdc` differs from PM website PnL (which marks open positions).                                                                                                                    |          |       |          |                 |          |           |
| `applied_filters`             | object         | Resolved `from` (unix), `to` (unix), and `window_days` (only set when default 30d window was used).                                                                                                                                                       |          |       |          |                 |          |           |
| `partial`                     | boolean        | `true` if the underlying walk hit the 30s budget and returned incomplete data.                                                                                                                                                                            |          |       |          |                 |          |           |
| `sources`                     | object         | Breakdown: `window_trades`, `window_activity`, per-event-type counts, `cashflow_breakdown` (buys/sells/settlements), `fifo_breakdown` (matcher diagnostics including `total_realized_pnl_usdc`). For transparency.                                        |          |       |          |                 |          |           |
| `trades`                      | array          | Only present when `include_trades=1`. One entry per fill.                                                                                                                                                                                                 |          |       |          |                 |          |           |

## Understanding cashflow PnL vs realized PnL

The most common source of confusion with this endpoint is "why don't `actual_pnl_usdc` and `total_realized_pnl_usdc` agree?" — they're answering different questions.

**Worked example.** A wallet's first 30 days:

```
Day  | Action                | actual_pnl_usdc | total_realized_pnl_usdc
-----+-----------------------+-----------------+-------------------------
 1   | BUY  100 YES @ $0.40  |     -$40.00     |        $0.00
 5   | SELL  50 YES @ $0.60  |     -$10.00     |       +$10.00
10   | BUY  100 YES @ $0.50  |     -$60.00     |       +$10.00
15   | SELL  80 YES @ $0.55  |     -$16.00     |       +$17.20
30   | (still hold 70)       |     -$16.00     |       +$17.20
```

**Final state explained:**

* `actual_pnl_usdc = -$16` — the wallet paid out \$16 more USDC than it received over the 30 days.
* `total_realized_pnl_usdc = +$17.20` — actual trading profit on the 130 shares the wallet closed.
* The wallet still HOLDS 70 shares (cost basis \$32.20). Cashflow counts that as money "spent". Realized ignores it.

**Reconciliation:** `cashflow_pnl + cost_basis_of_open_positions ≈ realized_pnl`. In this example: `-$16 + $32.20 = $16.20 ≈ $17.20` (small rounding from WAVG cost basis updates).

**Which to use:**

* For "did the wallet make money trading?" → **`total_realized_pnl_usdc`**. This is what trading platforms typically call "PnL" and what users intuitively expect.
* For "how much net cash flowed through this wallet?" → `actual_pnl_usdc`. Useful for liquidity analysis and the slippage formula, but **not** what most people mean by "PnL".
* For copy-trading slippage analysis → use `slippage_*` fields (which are derived from cashflow). The slippage formula needs apples-to-apples cashflow comparison; this is why `actual_pnl_usdc` is defined as cashflow.

**One-line summary:** realized = closed-trade profit; cashflow = USDC delta in/out of the wallet. They reconcile when the wallet holds zero open inventory.

***

## Example: default 30-day window

Request:

```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
  "https://api.polynode.dev/v2/copy-pnl/0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7"
```

Response (`200 OK`):

```json theme={null}
{
  "wallet": "0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
  "actual_pnl_usdc": -5697.9,
  "backtest_copy_pnl_usdc": -8932.81,
  "slippage_amount_usdc": 3234.91,
  "slippage_cost_rate_pct": 56.77,
  "toxic_for_copying": true,
  "trade_count": 1726,
  "avg_entry_prob_weighted": 0.4892,
  "avg_hold_seconds_weighted": null,
  "positions_closed": 142,
  "total_realized_pnl_usdc": -2412.06,
  "pnl_definition": "cashflow",
  "applied_filters": {
    "from": 1774916101,
    "to": 1777508101,
    "window_days": 30
  },
  "partial": false,
  "sources": {
    "window_trades": 1726,
    "window_activity": 51,
    "activity_breakdown": { "redemption": 51 },
    "cashflow_breakdown": {
      "actual_buy_cost": 85766.5,
      "actual_sell_rev": 77266.87,
      "settlement_in": 2801.73,
      "settlement_out": 0
    },
    "fifo_breakdown": {
      "over_sells": 3,
      "unresolved_activity": 0,
      "total_abs_pnl_usdc": 4823.18,
      "total_realized_pnl_usdc": -2412.06
    },
    "fetch_ms": 1361
  }
}
```

Reading this response: the wallet's **realized** trading P/L is \*\*-$2,412** (lost ~$2.4K on 142 positions that closed in the window). The **cashflow** says \*\*-$5,697** — the extra $3,285 is the cost basis of positions still open at window-end (inventory the wallet bought but hasn't sold yet). For "did this wallet make money?" the answer is `total_realized_pnl_usdc` — and the answer is no.

## Example: 7-day window via preset

Request:

```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
  "https://api.polynode.dev/v2/copy-pnl/0x9977760c6bd6f824cac834d1a36ee99478d63020?period=7d"
```

Response (`200 OK`):

```json theme={null}
{
  "wallet": "0x9977760c6bd6f824cac834d1a36ee99478d63020",
  "actual_pnl_usdc": 8082.45,
  "backtest_copy_pnl_usdc": 7077.07,
  "slippage_amount_usdc": 1005.38,
  "slippage_cost_rate_pct": 12.44,
  "toxic_for_copying": false,
  "trade_count": 1753,
  "avg_entry_prob_weighted": 0.5210,
  "avg_hold_seconds_weighted": null,
  "positions_closed": 87,
  "total_realized_pnl_usdc": 6428.91,
  "pnl_definition": "cashflow",
  "applied_filters": {
    "from": 1776903319,
    "to": 1777508119,
    "window_days": null
  },
  "partial": false,
  "sources": {
    "window_trades": 1753,
    "window_activity": 144,
    "activity_breakdown": { "redemption": 144 },
    "cashflow_breakdown": {
      "actual_buy_cost": 339438.1,
      "actual_sell_rev": 596.28,
      "settlement_in": 346924.27,
      "settlement_out": 0
    },
    "fifo_breakdown": {
      "over_sells": 0,
      "unresolved_activity": 0,
      "total_abs_pnl_usdc": 12857.82,
      "total_realized_pnl_usdc": 6428.91
    },
    "fetch_ms": 632
  }
}
```

## Example: explicit date range

Request — score the window from April 15 to April 25 inclusive:

```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
  "https://api.polynode.dev/v2/copy-pnl/0x9977760c6bd6f824cac834d1a36ee99478d63020?from=2026-04-15&to=2026-04-25"
```

Response (`200 OK`):

```json theme={null}
{
  "wallet": "0x9977760c6bd6f824cac834d1a36ee99478d63020",
  "actual_pnl_usdc": -70450.73,
  "backtest_copy_pnl_usdc": -74413.34,
  "slippage_amount_usdc": 3962.61,
  "slippage_cost_rate_pct": 5.62,
  "toxic_for_copying": false,
  "trade_count": 3787,
  "pnl_definition": "cashflow",
  "applied_filters": {
    "from": 1776211200,
    "to": 1777075200,
    "window_days": null
  },
  "partial": false,
  "sources": { "...": "..." }
}
```

## Example: per-fill drill-down

Request:

```bash theme={null}
curl -H "x-api-key: $YOUR_KEY" \
  "https://api.polynode.dev/v2/copy-pnl/0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7?period=7d&include_trades=1"
```

Response (`200 OK`, truncated):

```json theme={null}
{
  "wallet": "0x0c73d8abc3cd918bc62d0204e33ee05c3f5a68e7",
  "actual_pnl_usdc": -5027.89,
  "backtest_copy_pnl_usdc": -6783.42,
  "slippage_amount_usdc": 1755.53,
  "slippage_cost_rate_pct": 34.92,
  "toxic_for_copying": true,
  "trade_count": 838,
  "pnl_definition": "cashflow",
  "trades": [
    {
      "ts": 1777232322,
      "side": "BUY",
      "price": 0.7916,
      "shares": 252.66,
      "actual_usd": 200,
      "backtest_usd": 204
    },
    {
      "ts": 1777232322,
      "side": "SELL",
      "price": 0.21,
      "shares": 48,
      "actual_usd": 10.08,
      "backtest_usd": 9.88
    }
  ]
}
```

## Errors

`400 Invalid period`:

```json theme={null}
{ "error": "Invalid period. Allowed: 7d, 14d, 30d, 60d, 90d, 180d" }
```

`401 No API key`:

```json theme={null}
{ "error": "API key required. Pass via ?key= or x-api-key header." }
```

`403 Free tier`:

```json theme={null}
{ "error": "V2 endpoints require a paid plan. See polynode.dev/pricing for details." }
```

`429 Rate limited`:

```json theme={null}
{
  "error": "copy-pnl rate limited (1 req per 5s per key).",
  "retryAfterMs": 4231
}
```

The `Retry-After` header (in seconds, rounded up) is also set on 429 responses. The rate limit is shared across all `/v2/copy-pnl/*` calls per API key — calling the endpoint with different wallets or different params still counts toward the same 1 req per 5 seconds budget.

## Notes

* **Wallet address is case-insensitive**. Internally lowercased.
* **Time precision**: when `from` is `YYYY-MM-DD`, it resolves to that date at 00:00 UTC. Pass unix seconds for sub-day precision.
* **Settlement events** (redemption, merge, split, neg\_risk\_conversion) are processed at face value on both `actual_pnl` and `backtest_copy_pnl`, so they cancel out in `slippage_amount` but remain in the absolute PnL numbers.
* **High-volume wallets**: the underlying walk is paginated by time bucket and runs in parallel. Wallets with up to \~1.6M fills in the window have been validated. Beyond that, pass a tighter window or expect `partial: true`.
