# HTTP — `GET /v1/portfolioHistory`

Account value over time, for equity, cumulative PnL and return charts. Public
endpoint. [`GET /v1/clearinghouseState`](/api/clearinghouse-state) reports the
account now, and [`GET /v1/userFills`](/api/user-fills) and
[`GET /v1/userFunding`](/api/user-funding) report events, not valuations.

```http
GET /v1/portfolioHistory?user=0x0000000000000000000000000000000000000001&startTime=1791795600000&interval=1h
```

| Param | Required | Meaning |
| --- | --- | --- |
| `user` | yes | EVM address. Missing fails with `invalid_query`; malformed with `bad_request`. |
| `startTime` | yes | Inclusive start, Unix milliseconds. `0` reads the whole history. Missing fails with `invalid_query`. |
| `endTime` | no | Inclusive end, Unix milliseconds; defaults to now. `startTime > endTime` fails with `invalid_time_range`. |
| `interval` | no | `5m`, `1h` or `1d`. Any other value fails with `invalid_query`. See [Intervals and retention](#intervals-and-retention) for the default. |

```jsonc
{
  "status": "ok",
  "data": {
    "user": "0x0000000000000000000000000000000000000001",
    "interval": "1h",
    "points": [
      {
        "timeMs": 1791795605000,
        "accountValue": "10000",
        "totalCashUsdc": "10000",
        "unrealizedPnl": "0",
        "netDepositsUsdc": "10000",
        "valuationQuality": "cash_only"
      },
      {
        "timeMs": 1791799205000,
        "accountValue": "10038.2",
        "totalCashUsdc": "9998.2",
        "unrealizedPnl": "40",
        "netDepositsUsdc": "10000",
        "valuationQuality": "marked"
      },
      {
        "timeMs": 1791802805000,
        "accountValue": null,
        "totalCashUsdc": "7998.2",
        "unrealizedPnl": null,
        "netDepositsUsdc": "8000",
        "valuationQuality": "missing_marks"
      },
      {
        "timeMs": 1791806405000,
        "accountValue": "8098.2",
        "totalCashUsdc": "7998.2",
        "unrealizedPnl": "100",
        "netDepositsUsdc": "8000",
        "valuationQuality": "marked"
      },
      {
        "timeMs": 1791806861123,
        "accountValue": "8103.2",
        "totalCashUsdc": "7998.2",
        "unrealizedPnl": "105",
        "netDepositsUsdc": "8000",
        "valuationQuality": "marked"
      }
    ]
  },
  "server_time_ms": 1791806861180
}
```

The account in this example deposited 10,000 USDC, bought 0.1 BTC at 60,000
with a 1.80 fee before 10:00 UTC, and requested a 2,000 USDC withdrawal
before 11:00 UTC that is still pending. No BTC price was available during the
11:00 hour. The last point is the account at 12:07:41 UTC, when the request
was served.

| Field | Meaning |
| --- | --- |
| `interval` | The interval served. When the request names none, the server picks it. |
| `timeMs` | When the account was valued: the sample time for a stored point, the request time for the last point. |
| `accountValue` | `totalCashUsdc + unrealizedPnl`, the `accountValue` of `clearinghouseState`. `null` with `missing_marks`. |
| `totalCashUsdc` | Cash minus pending withdrawals, the `totalCashUsdc` of `clearinghouseState`. |
| `unrealizedPnl` | Sum of `size × (mark − entry price)` over open positions. Each position is marked with the oracle price of its own venue. `0` without an open position, `null` with `missing_marks`. |
| `netDepositsUsdc` | Credited deposits minus withdrawals that are requested and not rejected. |
| `valuationQuality` | `marked`: every open position had a price. `cash_only`: no open position. `missing_marks`: an open position had no price, so the account has no value at that time. |

Decimals are strings rounded to 6 decimal places. `accountValue` equals
`totalCashUsdc + unrealizedPnl` exactly. `clearinghouseState` returns JSON
numbers, so compare the two endpoints with a tolerance.

## PnL and return

Cumulative PnL at a point is `accountValue - netDepositsUsdc`. Return is that
PnL divided by `netDepositsUsdc`, when `netDepositsUsdc` is positive. In the
example, PnL is 38.2 at 10:00, 98.2 at 12:00 and 103.2 at 12:07. A
`missing_marks` point has no PnL.

Fees, funding and realized PnL are already in `totalCashUsdc`, so they are in
the PnL. Deposits and withdrawals move `accountValue` and `netDepositsUsdc` by
the same amount, so they are not.

## Withdrawals

A withdrawal leaves `totalCashUsdc` and `netDepositsUsdc` when it is
requested, as it leaves `clearinghouseState` cash. If the withdrawal is
rejected, the amount returns to both fields. If it completes, it stays out of
both. The PnL does not move at any of these steps.

## Intervals and retention

Each interval is a separate series sampled from the same 5-minute samples.

| Interval | Point | Kept for |
| --- | --- | --- |
| `5m` | The first valued sample of each 5-minute UTC bucket. | 7 days |
| `1h` | The first valued sample of each UTC hour. | 92 days |
| `1d` | The first valued sample of each UTC day. | The life of the account |

A valued sample is `marked` or `cash_only`. A `missing_marks` point is
replaced by the next valued sample of its bucket, so a short price outage does
not blank a whole hour or day.

Without `interval`, the server measures the window from the later of
`startTime` and the account's first sample, to `endTime`. It serves `5m` for a
window up to one day, `1h` up to 31 days and `1d` beyond. If the picked
interval is no longer kept at the window start, it serves the next coarser
one: a one-day window that starts 8 days ago is served at `1h`. With an
explicit `interval`, a range older than its retention returns no stored
points.

## Gaps

History starts at the account's first sample. Nothing before it is
available.

The server samples every account every 5 minutes. A missed sample, such as
during a restart, leaves no point: points are not evenly spaced, and a missing
point is not a zero. Draw a gap for a `missing_marks` point.

An address that is unknown, or whose account never held cash, a position or
a transfer, returns an empty `points` array.

## The last point

When the range contains the present, the last point is the account now. The
range contains the present when `endTime` is omitted or is not in the past.
The server values the account when it serves the request, from the ledger and
with the oracle prices `clearinghouseState` uses. The last point is not
stored. It can trail `clearinghouseState` by the time the ledger takes to
record a trade, normally a few milliseconds.

Requests count toward the shared HTTP limit of
[600 per minute per IP](/api/http-public#rate-limits).
