> 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/api-reference/guides/portfolio.md).

# Portfolio & Account State

Consolidated account state, withdrawable balance, fee schedule, enriched positions, and the portfolio time-series, across the SDKs and CLI.

The portfolio endpoints answer four questions about an account: what it is worth right now, what can be withdrawn, what it gets charged, and how it has performed over time. They are a small set of authenticated REST endpoints plus enriched per-position risk fields. Every interface (the Rust, TypeScript and Python SDKs and the CLI) reaches the same routes on the gateway documented under [APIs & Rates](https://docs.nexus.xyz/exchange/apis-and-rates).

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

### Getting it

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

The CLI is distributed through [`nexus-exchange-cli`](https://github.com/nexus-xyz/nexus-exchange-cli) releases. See [SDKs & Tools](/api-reference/guides/sdks.md) for every client.

### 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](https://docs.nexus.xyz/exchange/trading/quickstart).

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 testnet today, and mainnet will follow. Select the network when you construct the client. See [Networks](/api-reference/guides/networks.md) for how each interface does it and how API keys bind to a network, and [APIs & Rates](https://docs.nexus.xyz/exchange/apis-and-rates) 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 leave the account: engine-authoritative free margin floored at zero, `max(0, available_margin)`. A withdrawal also needs the account flat: while it holds a position, a working order that reserves margin, or funding still owed, every way out (a withdrawal, a bridge withdrawal or a transfer out) is refused with `WITHDRAW_BLOCKED_BY_OPEN_POSITION`, whatever this reads. Once it is flat, this is the whole balance, unless withdrawals are disabled or an uncovered system loss is outstanding, which refuse every withdrawal. An underwater account clamps to `"0"` and is never reported negative. The value comes 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. That is the forward-looking schedule rate, not a realized average over past fills.

| Field                  | Notes                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `maker_fee_bps`        | Headline basis points. May be negative; zero is a sentinel when `schedule=unknown`   |
| `taker_fee_bps`        | Headline basis points; zero is a sentinel when `schedule=unknown`                    |
| `tier`                 | Currently always `base`; there are no per-account tiers yet                          |
| `schedule`             | `per_market`, `reference`, or `unknown`; treat as an open string                     |
| `markets`              | Rates for markets in the retained fill buffer; sorted by `market_id`                 |
| `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                                             |

When `schedule` is `per_market`, use the returned `markets` rows as the authoritative rates. The top-level pair is only their deterministic modal summary. The capped in-memory fill buffer can reset or evict old markets, so an absent row is not evidence that the account never traded that market. `reference` means the retained buffer has no market with a mirrored schedule and the top-level pair is the deterministic modal rate across all mirrored venue markets. For both modal calculations, the pair with the highest occurrence count wins, with ties resolved to the lexicographically smallest `(maker_fee_bps, taker_fee_bps)` pair. `unknown` means market parameters are unavailable. `markets` is empty and both headline bps values are zero sentinels, not a free-fee schedule. No per-account tier or discount program exists today. Treat `schedule` as open-ended, 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 that range, the request is rejected with `400`.

The response echoes the `window` and the `cadence_ms` that were served. Read them back rather than assuming the values you sent, so your chart axes carry the served values.

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, take the difference of adjacent points yourself.

The response also carries a top-level `equity`: the account's equity at read time, as a decimal string. The last point can be up to one cadence old (5 minutes for `day`), so use this field, not the last point, when you need the current figure. It is `null` when the server holds no balance for the account yet, which does not mean zero.

Next to it, `balance` is the collateral balance inside that `equity`, read at the same instant, so `equity - balance` is the account's unrealized PnL at read time. It is `null` exactly when `equity` is.

### Enriched position fields

Positions returned by `/account/state` (and `/positions`) carry per-position risk detail alongside `symbol`, `side`, `size`, `entryPrice`, `unrealizedPnl` and `realizedPnl`:

| Field           | Meaning                                                                |
| --------------- | ---------------------------------------------------------------------- |
| `notional`      | `abs(size) × mark price`                                               |
| `initialMargin` | Initial margin held against the position, under the cross-margin model |
| `roe`           | Return on initial margin: `unrealizedPnl / initialMargin`              |
| `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      |

**The names are CCXT's unified vocabulary**, so a `ccxt.nexus()` client reads this shape directly. The fields CCXT has no counterpart for keep the venue's own spelling: `size`, `roe`, `max_leverage`, `funding_paid`. `GET /account` is not this shape. It is relayed from the matching engine and answers a narrower object under the engine's own names.

The server computes these on the low-latency read path rather than by round-tripping to the matching engine, which 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 `initialMargin`. That expression collapses to `1 / initial_margin_rate`, which is a per-market constant, not the position's real leverage, so it would be wrong for every position in the market.

**`funding_paid` is paid-positive.** A positive value means the position has *paid* funding, and 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 write a test for it.

### Reading these values safely

**Monetary values are decimal strings, not numbers.** `equity`, `pnl`, `volume`, `withdrawable`, `notional` 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 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`, `initialMargin`, `roe`, `leverage` and `max_leverage`) can be:

1. **a value**: computed and authoritative;
2. **`null`**: reported, but not computable, and 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 server never reported the value.

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 this happens under any real load. The consolidated endpoint backs both halves with one coherent read.

**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](/api-reference/guides/rate-limits.md) and back off rather than retrying immediately. 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 five 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.Testnet,
  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.symbol} ${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": [
    {
      "symbol": "BTC-USDX-PERP",
      "side": "Long",
      "size": "0.25",
      "entryPrice": "84000.00",
      "unrealizedPnl": "500.00",
      "notional": "21500.00",
      "initialMargin": "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. At a mark price of `86000`, `notional` is `0.25 × 86000`, `unrealizedPnl` is `0.25 × (86000 − 84000)`, `initialMargin` is `notional × 1/max_leverage`, `roe` is `unrealizedPnl / initialMargin`, and `withdrawable` equals the summary's `total_equity − margin_used` because there are no open orders reserving margin. Note the two spellings in that last clause. `margin_used` on the SUMMARY object is the account aggregate and keeps its name. `initialMargin` on a position is the per-position requirement.

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](https://docs.nexus.xyz/exchange/apis-and-rates)
* [Exchange REST](https://docs.nexus.xyz/exchange/apis-and-rates/exchange-rest)
* [Rate Limits](/api-reference/guides/rate-limits.md)
* [SDKs & Tools](/api-reference/guides/sdks.md)
* [Quickstart](https://docs.nexus.xyz/exchange/trading/quickstart)

> **Status:** development preview on testnet. These routes ship in OpenAPI spec v0.7.2 and are current in v0.9.131. Pin to a spec version in production and check each SDK's release notes before upgrading. `/account/fees` has no per-account tier or discount program today, 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.nexus.xyz/api-reference/guides/portfolio.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
