> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/interfaces/portfolio.md).

# Portfolio & Account State

The portfolio surface answers four questions about an account in one place: what is it worth right now, what can actually be withdrawn, what does it get charged, and how has it performed over time. It is exposed as a small set of authenticated REST endpoints plus enriched per-position risk fields, and every interface — the Rust, TypeScript and Python SDKs and the CLI — reaches the same routes on the gateway documented under [APIs & Rates](/exchange/apis-and-rates.md).

If you are building a portfolio view, read [Reading these values safely](#reading-these-values-safely) before you render anything. Several fields are deliberately nullable, one sign convention is easy to invert, and there is one race condition that a naive two-call implementation will hit in production.

### Getting it

```bash
npm install @nexus-xyz/exchange-ts    # TypeScript
cargo add nexus-exchange              # Rust

# Python is not on PyPI yet — install from source:
pip install git+https://github.com/nexus-xyz/nexus-exchange-py
```

The CLI is distributed through [`nexus-exchange-cli`](https://github.com/nexus-xyz/nexus-exchange-cli) releases. See the [Interfaces overview](/interfaces/interfaces.md) for all four clients.

### Authentication

Every route on this page is account-scoped and requires an HMAC API key — there is no public or unauthenticated variant. Requests carry `X-API-Key`, `X-Timestamp` and `X-Signature`, and the timestamp must be within 30 seconds of server time. The SDKs and CLI sign for you. The full flow, with runnable cURL, is in the [Quickstart](/exchange/trading/quickstart.md).

Keep the API secret out of source and out of shell history — it is shown once at creation and cannot be retrieved later. The examples below read it from the environment.

### Choosing a network

The Exchange runs on Nexus Testnet today; mainnet will follow. Select the network when you construct the client — see [Networks](/interfaces/networks.md) for how each interface does it and how API keys bind to a network, and [APIs & Rates](/exchange/apis-and-rates.md) for the current base URL. The HMAC key these routes require is scoped to the network it was created on.

### Reference

| Method | Path                                        | Auth | Description                                                            |
| ------ | ------------------------------------------- | ---- | ---------------------------------------------------------------------- |
| GET    | `/account/state`                            | HMAC | Portfolio summary **and** every open position, from one coherent read  |
| GET    | `/account/summary`                          | HMAC | Portfolio summary alone — the same object embedded in `/account/state` |
| GET    | `/account/fees`                             | HMAC | Effective fee schedule for the account                                 |
| GET    | `/account/portfolio-history?window=&limit=` | HMAC | Equity, cumulative PnL and cumulative volume time-series               |

Per-SDK entry points:

| Interface  | Consolidated state      | Fee schedule           | Time-series                                        |
| ---------- | ----------------------- | ---------------------- | -------------------------------------------------- |
| Rust       | `fetch_account_state()` | `fetch_account_fees()` | `fetch_portfolio_history(window, limit)`           |
| Python     | `fetch_account_state()` | `fetch_account_fees()` | `fetch_portfolio_history(window, limit)`           |
| TypeScript | `getAccountState()`     | `getAccountFees()`     | `getPortfolioHistory({ window, limit })`           |
| CLI        | `nexus account state`   | `nexus account fees`   | `nexus account portfolio-history --window --limit` |

### Consolidated account state

`GET /account/state` returns the portfolio aggregates and all open positions built from **one** server-side read. Because both halves come from the same snapshot, `summary.open_positions_count` always equals the length of `positions`.

The summary carries `collateral`, `total_equity`, `total_unrealized_pnl`, `total_realized_pnl_24h`, `total_volume_24h`, `open_positions_count`, `open_orders_count`, `margin_used`, `available_margin` and `withdrawable`.

**`withdrawable`** is the balance that can actually leave the account: engine-authoritative free margin floored at zero, `max(0, available_margin)`. Free margin already nets each position's initial margin and every pre-trade order reservation out of equity, so this is the amount a withdrawal can draw on — not `total_equity`, and not `collateral`. An underwater account clamps to `"0"` and is never reported negative. It is derived from the authoritative margin view, so when that view is unavailable the endpoint **fails closed with `502`** rather than returning a locally estimated number.

### Fee schedule

`GET /account/fees` reports what the venue charges the account today — the forward-looking schedule rate, not a realized average over past fills.

| Field                  | Notes                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `maker_fee_bps`        | Basis points. **May be negative**, which means the maker is paid a rebate            |
| `taker_fee_bps`        | Basis points — `5` is 0.05%                                                          |
| `tier`                 | Currently always `base`; there are no per-account tiers yet                          |
| `schedule`             | Scope of the reported rate. Currently always `standard`                              |
| `volume_30d`           | Rolling 30-day traded notional, decimal string. Best-effort — see the flag below     |
| `volume_30d_estimated` | `true` when `volume_30d` may **undercount** (the source fill buffer was at capacity) |
| `discounts`            | Active discounts. Currently always empty                                             |

`tier`, `schedule` and `discounts` are provisional: the fee model is still being finalized, and `discounts` entries have no guaranteed properties yet. Treat the two `_bps` integers as the stable part of this response and do not branch on a discount shape that does not exist.

### Portfolio time-series

`GET /account/portfolio-history` returns equity, cumulative trading PnL and cumulative traded notional over a window, downsampled server-side. Points are **oldest first**.

| `window` | Cadence | Max points | Span  |
| -------- | ------- | ---------- | ----- |
| `day`    | 5 min   | 288        | 24 h  |
| `week`   | 1 h     | 168        | 7 d   |
| `month`  | 6 h     | 120        | 30 d  |
| `all`    | 1 d     | 366        | \~1 y |

Omitting `window` gives `day`. A value outside that set is rejected with `400` (`invalid_window`); if the parameter is repeated, the first value is used. `limit` must be between `1` and `366` — inside that range it narrows the result and is **clamped** to the window's capacity rather than rejected, so asking for 366 points of `day` returns 288 rather than an error; outside it, the request is rejected with `400`.

The response echoes the `window` and the `cadence_ms` that were actually served. Read them back rather than assuming the values you sent: it is the difference between labelling an axis correctly and labelling it plausibly.

Each point carries `timestamp_ms`, `equity`, `pnl` and `volume`. `pnl` and `volume` are **cumulative up to that sample**, not per-interval — to chart per-interval activity, difference adjacent points yourself.

### Enriched position fields

Positions returned by `/account/state` (and `/positions`) carry per-position risk detail alongside `market_id`, `side`, `size`, `entry_price`, `unrealized_pnl` and `realized_pnl`:

| Field            | Meaning                                                                |
| ---------------- | ---------------------------------------------------------------------- |
| `notional_value` | `abs(size) × mark price`                                               |
| `margin_used`    | Initial margin held against the position, under the cross-margin model |
| `roe`            | Return on initial margin: `unrealized_pnl / margin_used`               |
| `max_leverage`   | Maximum leverage the market allows, from its risk params               |
| `leverage`       | The account's leverage multiplier for this position                    |
| `funding_paid`   | Cumulative funding on the position — see the sign convention below     |

These are computed on the low-latency read path rather than by round-tripping to the matching engine, which is what keeps the endpoint fast. The trade-off is that when one of their inputs is not available on that path, the field is `null` and a companion `<field>_error` carries a machine-readable reason instead of a fabricated number.

**`leverage` is currently always `null`,** with `leverage_error` set to `margin_state_not_mirrored`: deriving it needs the account's leverage setting or its allocated margin, and neither is available on the read path. Do not reconstruct it from `margin_used` — that expression collapses to `1 / initial_margin_rate`, which is a per-market constant, not the position's real leverage. Showing that as leverage would be confidently wrong on every position in the market.

**`funding_paid` is paid-positive.** A positive value means the position has *paid* funding; a negative value means it has *received* funding. It is always present, `"0"` before any funding accrues, and bounded by the funding history the venue retains. Inverting this sign turns a cost into income on a P\&L screen, so it is worth a test.

### Reading these values safely

**Monetary values are decimal strings, not numbers.** `equity`, `pnl`, `volume`, `withdrawable`, `notional_value` and the rest are arbitrary-precision decimals serialized as strings so they are lossless. Parse them with a decimal type. Passing them through a float — `parseFloat`, `float()`, `as f64` — reintroduces exactly the rounding error the string encoding exists to prevent. Leverage fields (`leverage`, `max_leverage`) are genuine JSON numbers.

**Derived fields have three states, not two.** Each of the five computed position fields — `notional_value`, `margin_used`, `roe`, `leverage` and `max_leverage` — can be:

1. **a value** — computed and authoritative;
2. **`null`** — reported, but not computable; the paired `<field>_error` says why;
3. **absent** — the server predates the field.

Collapsing any of these into `0` invents data. "Not reported", "not computable" and "zero" are three different answers to a user asking what their position is worth, and only one of them is a number. Render the missing cases as an explicit gap — the CLI prints `-` — and surface the `<field>_error` when you have it. The same applies to `withdrawable`: it is optional in the schema, so an older deployment can omit it, and defaulting that to `"0"` would tell someone they have nothing available when the truth is that nobody asked.

Those five are exactly the fields carrying a companion `<field>_error`. `funding_paid` is not one of them — it is always present, so there is no `funding_paid_error` to branch on.

**Prefer `/account/state` over two calls.** Fetching `/account/summary` and `/positions` separately is two independent requests against a live account. A fill landing between them returns an aggregate that disagrees with the position list — `open_positions_count` says three, the array has four — and the window is real under any load. The consolidated endpoint exists so that one coherent read backs both halves.

**Handle the failure codes distinctly.** `401` is a credential or clock problem — check that `X-Timestamp` is within 30 seconds of server time before assuming the key is wrong. `429` means you exceeded the rate budget; see [Rate Limits](/interfaces/rate-limits.md) and back off rather than retrying immediately. Note that three routes on this page — `/account/summary`, `/fills` and `/account/portfolio-history` — each cost **five** units of that budget rather than one, so polling them on a tight loop exhausts a Pro caller's 20/s allowance four times faster than a request count suggests. `/account/state` costs one and returns the summary and positions together, which is the cheaper way to read both. `502` on `/account/state` and `/account/summary` means the authoritative margin view was unreachable and the server declined to guess — retry, and do not fall back to a locally computed `withdrawable`.

### Example

```bash
export NEXUS_API_KEY=nx_7f3a1b...          # the key ID is not secret

# Prompt for the secret instead of typing it inline — an `export
# NEXUS_API_SECRET=...` would leave it in your shell history.
read -rs NEXUS_API_SECRET && export NEXUS_API_SECRET

nexus account state
nexus account fees
nexus account portfolio-history --window week
```

```typescript
import { Client, Network } from "@nexus-xyz/exchange-ts";

const client = new Client({
  network: Network.Stable,
  apiKey: process.env.NEXUS_API_KEY!,
  apiSecret: process.env.NEXUS_API_SECRET!,
});

// One coherent read: open_positions_count cannot disagree with positions.length.
const { summary, positions } = await client.getAccountState();

// Absent is not zero — say so rather than defaulting.
console.log(`withdrawable: ${summary.withdrawable ?? "<not reported>"}`);

for (const p of positions) {
  // null carries a reason in the companion field; absent carries nothing.
  const roe = p.roe ?? (p.roe_error ? `<${p.roe_error}>` : "<not reported>");
  // Paid-positive: a positive funding_paid means this position paid.
  console.log(`${p.market_id} ${p.side} ${p.size}  roe=${roe}  funding_paid=${p.funding_paid}`);
}

// The response echoes what was served — read it back, don't assume.
const history = await client.getPortfolioHistory({ window: "week" });
console.log(`${history.window} @ ${history.cadence_ms}ms, ${history.points.length} points`);
```

```json
{
  "summary": {
    "collateral": "25000.00",
    "total_equity": "25500.00",
    "total_unrealized_pnl": "500.00",
    "margin_used": "1075.00",
    "available_margin": "24425.00",
    "withdrawable": "24425.00",
    "open_positions_count": 1,
    "open_orders_count": 0
  },
  "positions": [
    {
      "market_id": "BTC-USDX-PERP",
      "side": "Long",
      "size": "0.25",
      "entry_price": "84000.00",
      "unrealized_pnl": "500.00",
      "notional_value": "21500.00",
      "margin_used": "1075.00",
      "roe": "0.4651",
      "max_leverage": 20,
      "leverage": null,
      "leverage_error": "margin_state_not_mirrored",
      "funding_paid": "3.21"
    }
  ]
}
```

The numbers above are self-consistent, which is worth tracing once: at a mark price of `86000`, `notional_value` is `0.25 × 86000`, `unrealized_pnl` is `0.25 × (86000 − 84000)`, `margin_used` is `notional_value × 1/max_leverage`, `roe` is `unrealized_pnl / margin_used`, and `withdrawable` equals `total_equity − margin_used` because there are no open orders reserving margin.

Runnable end-to-end examples live in the SDK repositories: [`examples/portfolio.ts`](https://github.com/nexus-xyz/nexus-exchange-ts/blob/main/examples/portfolio.ts) and [`examples/portfolio.rs`](https://github.com/nexus-xyz/nexus-exchange-rs/blob/main/examples/portfolio.rs).

### Related

* [APIs & Rates](/exchange/apis-and-rates.md)
* [Exchange REST](/exchange/apis-and-rates/exchange-rest.md)
* [Rate Limits](/interfaces/rate-limits.md)
* [Interfaces overview](/interfaces/interfaces.md)
* [Quickstart](/exchange/trading/quickstart.md)

> **Status:** development preview on testnet. These routes ship in OpenAPI spec v0.7.2; pin to a spec version in production and check each SDK's release notes before upgrading. `tier`, `schedule` and `discounts` on `/account/fees` are provisional and finalize with the fee model, and `leverage` reports `null` until the margin state it needs is mirrored. Testnet credentials and balances have no real-world value.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.nexus.xyz/interfaces/portfolio.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
